The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ssosettings/api/api_test.go

394 lines
12 KiB

package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestSSOSettingsAPI_Update(t *testing.T) {
type TestCase struct {
desc string
key string
body string
action string
scope string
expectedError error
expectedServiceCall bool
expectedStatusCode int
}
tests := []TestCase{
{
desc: "successfully updates SSO settings",
key: social.GitHubProviderName,
body: `{"settings": {"enabled": true}}`,
action: "settings:write",
scope: "settings:auth.github:*",
expectedError: nil,
expectedServiceCall: true,
expectedStatusCode: http.StatusNoContent,
},
{
desc: "fails when action doesn't match",
key: social.GitHubProviderName,
body: `{"settings": {"enabled": true}}`,
action: "settings:read",
scope: "settings:auth.github:*",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails when scope doesn't match",
key: social.GitHubProviderName,
body: `{"settings": {"enabled": true}}`,
action: "settings:write",
scope: "settings:auth.github:read",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails when scope contains another provider",
key: social.GitHubProviderName,
body: `{"settings": {"enabled": true}}`,
action: "settings:write",
scope: "settings:auth.okta:*",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails with not found when key is empty",
key: "",
body: `{"settings": {"enabled": true}}`,
action: "settings:write",
scope: "settings:auth.github:*",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusNotFound,
},
{
desc: "fails with bad request when body contains invalid json",
key: social.GitHubProviderName,
body: `{ invalid json }`,
action: "settings:write",
scope: "settings:auth.github:*",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "fails with bad request when key was not found",
key: social.GitHubProviderName,
body: `{"settings": {"enabled": true}}`,
action: "settings:write",
scope: "settings:auth.github:*",
expectedError: ssosettings.ErrInvalidProvider.Errorf("invalid provider"),
expectedServiceCall: true,
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "fails with internal server error when service returns an error",
key: social.GitHubProviderName,
body: `{"settings": {"enabled": true}}`,
action: "settings:write",
scope: "settings:auth.github:*",
expectedError: errors.New("something went wrong"),
expectedServiceCall: true,
expectedStatusCode: http.StatusInternalServerError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
var input models.SSOSettings
_ = json.Unmarshal([]byte(tt.body), &input)
settings := models.SSOSettings{
Provider: tt.key,
Settings: input.Settings,
}
service := ssosettingstests.NewMockService(t)
if tt.expectedServiceCall {
service.On("Upsert", mock.Anything, settings).Return(tt.expectedError).Once()
}
server := setupTests(t, service)
path := fmt.Sprintf("/api/v1/sso-settings/%s", tt.key)
req := server.NewRequest(http.MethodPut, path, bytes.NewBufferString(tt.body))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgRole: org.RoleEditor,
OrgID: 1,
Permissions: getPermissionsForActionAndScope(tt.action, tt.scope),
})
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tt.expectedStatusCode, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
}
func TestSSOSettingsAPI_Delete(t *testing.T) {
type TestCase struct {
desc string
key string
action string
scope string
expectedError error
expectedServiceCall bool
expectedStatusCode int
}
tests := []TestCase{
{
desc: "successfully deletes SSO settings",
key: social.AzureADProviderName,
action: "settings:write",
scope: "settings:auth.azuread:*",
expectedError: nil,
expectedServiceCall: true,
expectedStatusCode: http.StatusNoContent,
},
{
desc: "fails when action doesn't match",
key: social.AzureADProviderName,
action: "settings:read",
scope: "settings:auth.azuread:*",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails when scope doesn't match",
key: social.AzureADProviderName,
action: "settings:write",
scope: "settings:auth.azuread:read",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails when scope contains another provider",
key: social.AzureADProviderName,
action: "settings:write",
scope: "settings:auth.github:*",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails with not found when key is empty",
key: "",
action: "settings:write",
scope: "settings:auth.azuread:*",
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusNotFound,
},
{
desc: "fails with not found when key was not found",
key: social.AzureADProviderName,
action: "settings:write",
scope: "settings:auth.azuread:*",
expectedError: ssosettings.ErrNotFound,
expectedServiceCall: true,
expectedStatusCode: http.StatusNotFound,
},
{
desc: "fails with internal server error when service returns an error",
key: social.AzureADProviderName,
action: "settings:write",
scope: "settings:auth.azuread:*",
expectedError: errors.New("something went wrong"),
expectedServiceCall: true,
expectedStatusCode: http.StatusInternalServerError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service := ssosettingstests.NewMockService(t)
if tt.expectedServiceCall {
service.On("Delete", mock.Anything, tt.key).Return(tt.expectedError).Once()
}
server := setupTests(t, service)
path := fmt.Sprintf("/api/v1/sso-settings/%s", tt.key)
req := server.NewRequest(http.MethodDelete, path, nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgRole: org.RoleEditor,
OrgID: 1,
Permissions: getPermissionsForActionAndScope(tt.action, tt.scope),
})
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tt.expectedStatusCode, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
}
func TestSSOSettingsAPI_GetForProvider(t *testing.T) {
type TestCase struct {
desc string
key string
action string
scope string
expectedResult *models.SSOSettings
expectedError error
expectedServiceCall bool
expectedStatusCode int
}
tests := []TestCase{
{
desc: "successfully gets SSO settings",
key: "azuread",
action: "settings:read",
scope: "settings:auth.azuread:*",
expectedResult: &models.SSOSettings{
ID: "1",
Provider: "azuread",
Settings: make(map[string]interface{}),
Created: time.Now(),
Updated: time.Now(),
IsDeleted: false,
Source: models.DB,
},
expectedError: nil,
expectedServiceCall: true,
expectedStatusCode: http.StatusOK,
},
{
desc: "fails when action doesn't match",
key: "azuread",
action: "settings:write",
scope: "settings:auth.azuread:*",
expectedResult: nil,
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails when scope doesn't match",
key: "azuread",
action: "settings:read",
scope: "settings:auth.azuread:write",
expectedResult: nil,
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails when scope contains another provider",
key: "azuread",
action: "settings:read",
scope: "settings:auth.github:*",
expectedResult: nil,
expectedError: nil,
expectedServiceCall: false,
expectedStatusCode: http.StatusForbidden,
},
{
desc: "fails with not found when key was not found",
key: "nonexistant",
action: "settings:read",
scope: "settings:auth.nonexistant:*",
expectedResult: nil,
expectedError: ssosettings.ErrNotFound,
expectedServiceCall: true,
expectedStatusCode: http.StatusNotFound,
},
{
desc: "fails with internal server error when service returns an error",
key: "azuread",
action: "settings:read",
scope: "settings:auth.azuread:*",
expectedResult: nil,
expectedError: errors.New("something went wrong"),
expectedServiceCall: true,
expectedStatusCode: http.StatusInternalServerError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service := ssosettingstests.NewMockService(t)
if tt.expectedServiceCall {
service.On("GetForProviderWithRedactedSecrets", mock.AnythingOfType("*context.valueCtx"), tt.key).Return(tt.expectedResult, tt.expectedError).Once()
}
server := setupTests(t, service)
path := fmt.Sprintf("/api/v1/sso-settings/%s", tt.key)
req := server.NewRequest(http.MethodGet, path, nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgRole: org.RoleEditor,
OrgID: 1,
Permissions: getPermissionsForActionAndScope(tt.action, tt.scope),
})
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tt.expectedStatusCode, res.StatusCode)
if tt.expectedError == nil {
var data models.SSOSettings
require.NoError(t, json.NewDecoder(res.Body).Decode(&data))
}
require.NoError(t, res.Body.Close())
})
}
}
func getPermissionsForActionAndScope(action, scope string) map[int64]map[string][]string {
return map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{{
Action: action, Scope: scope,
}}),
}
}
func setupTests(t *testing.T, service ssosettings.Service) *webtest.Server {
t.Helper()
cfg := setting.NewCfg()
logger := log.NewNopLogger()
api := &Api{
Log: logger,
RouteRegister: routing.NewRouteRegister(),
AccessControl: acimpl.ProvideAccessControl(cfg),
SSOSettingsService: service,
}
api.RegisterAPIEndpoints()
return webtest.NewServer(t, api.RouteRegister)
}