Auth: Implement the SSO Settings List endpoint (#80769)
* add list endpoint & initial tests * add tests and ETag * format service_test.go * add list swagger param, generate openAPI, remove ETag, use RedactedPassword * correct swagger param name * Align tests to latest changes * use setting.RedactedValue() * add string assertion * lint & require no error on res.Body.Close() * add custom response type --------- Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com>
This commit is contained in:
co-authored by
Mihaly Gyongyosi
parent
40312c527b
commit
e241188f00
@@ -44,9 +44,9 @@ func ProvideApi(
|
||||
}
|
||||
|
||||
// generateFNVETag computes a FNV hash-based ETag for the SSOSettings struct
|
||||
func generateFNVETag(SSOSettings *models.SSOSettings) (string, error) {
|
||||
func generateFNVETag(input any) (string, error) {
|
||||
hasher := fnv.New64()
|
||||
data, err := json.Marshal(SSOSettings)
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -76,17 +76,28 @@ func (api *Api) RegisterAPIEndpoints() {
|
||||
})
|
||||
}
|
||||
|
||||
// swagger:route GET /v1/sso-settings sso_settings listAllProvidersSettings
|
||||
//
|
||||
// # List all SSO Settings entries
|
||||
//
|
||||
// You need to have a permission with action `settings:read` with scope `settings:auth.<provider>:*`.
|
||||
//
|
||||
// Responses:
|
||||
// 200: listSSOSettingsResponse
|
||||
// 400: badRequestError
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
func (api *Api) listAllProvidersSettings(c *contextmodel.ReqContext) response.Response {
|
||||
providers, err := api.getAuthorizedList(c.Req.Context(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get providers", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to list all providers settings", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, providers)
|
||||
}
|
||||
|
||||
func (api *Api) getAuthorizedList(ctx context.Context, identity identity.Requester) ([]*models.SSOSettings, error) {
|
||||
allProviders, err := api.SSOSettingsService.List(ctx)
|
||||
allProviders, err := api.SSOSettingsService.ListWithRedactedSecrets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -212,8 +223,12 @@ func (api *Api) removeProviderSettings(c *contextmodel.ReqContext) response.Resp
|
||||
return response.Empty(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// swagger:parameters listAllProvidersSettings
|
||||
type ListAllProvidersSettingsParams struct {
|
||||
}
|
||||
|
||||
// swagger:parameters getProviderSettings
|
||||
type GetProviderSettingsWrapper struct {
|
||||
type GetProviderSettingsParams struct {
|
||||
// in:path
|
||||
// required:true
|
||||
Provider string `json:"key"`
|
||||
@@ -236,6 +251,12 @@ type RemoveProviderSettingsParams struct {
|
||||
Provider string `json:"key"`
|
||||
}
|
||||
|
||||
// swagger:response listSSOSettingsResponse
|
||||
type ListSSOSettingsResponse struct {
|
||||
// in: body
|
||||
Body []models.SSOSettings `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:response getSSOSettingsResponse
|
||||
type GetSSOSettingsResponse struct {
|
||||
// in: body
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -367,6 +368,174 @@ func TestSSOSettingsAPI_GetForProvider(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSOSettingsAPI_List(t *testing.T) {
|
||||
type TestCase struct {
|
||||
desc string
|
||||
action string
|
||||
scope string
|
||||
expectedResult []*models.SSOSettings
|
||||
errFromService error
|
||||
wantErr bool
|
||||
expectedErrMessage string
|
||||
expectedServiceCall bool
|
||||
expectedStatusCode int
|
||||
}
|
||||
|
||||
tests := []TestCase{
|
||||
{
|
||||
desc: "successfully lists SSO settings",
|
||||
action: "settings:read",
|
||||
scope: "settings:auth.azuread:*",
|
||||
expectedResult: []*models.SSOSettings{
|
||||
{
|
||||
ID: "1",
|
||||
Provider: "azuread",
|
||||
Settings: make(map[string]interface{}),
|
||||
Source: models.DB,
|
||||
},
|
||||
},
|
||||
expectedServiceCall: true,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "returns empty list when the user has the action but the scope doesn't match any of the providerss scope",
|
||||
action: "settings:read",
|
||||
scope: "settings:auth.saml:write",
|
||||
expectedResult: []*models.SSOSettings{},
|
||||
expectedServiceCall: true,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "successfully lists SSO settings when scope contains wildcard",
|
||||
action: "settings:read",
|
||||
scope: "settings:*",
|
||||
expectedResult: []*models.SSOSettings{
|
||||
{
|
||||
ID: "1",
|
||||
Provider: "azuread",
|
||||
Settings: make(map[string]interface{}),
|
||||
Source: models.DB,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Provider: "github",
|
||||
Settings: make(map[string]interface{}),
|
||||
Source: models.DB,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Provider: "okta",
|
||||
Settings: make(map[string]interface{}),
|
||||
Source: models.System,
|
||||
},
|
||||
},
|
||||
expectedServiceCall: true,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "fails when action doesn't match",
|
||||
action: "madeupaction:read",
|
||||
scope: "madeupscope:*",
|
||||
wantErr: true,
|
||||
expectedErrMessage: "You'll need additional permissions to perform this action. Permissions needed: settings:read",
|
||||
expectedResult: nil,
|
||||
expectedServiceCall: false,
|
||||
expectedStatusCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
desc: "fails with internal server error when service returns an error",
|
||||
action: "settings:read",
|
||||
scope: "settings:auth.azuread:*",
|
||||
errFromService: errors.New("something went wrong"),
|
||||
expectedResult: nil,
|
||||
wantErr: true,
|
||||
expectedErrMessage: "Failed to list all providers settings",
|
||||
expectedServiceCall: true,
|
||||
expectedStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
service := ssosettingstests.NewMockService(t)
|
||||
|
||||
serviceResult := []*models.SSOSettings{
|
||||
{
|
||||
ID: "1",
|
||||
Provider: "azuread",
|
||||
Settings: make(map[string]interface{}),
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
IsDeleted: false,
|
||||
Source: models.DB,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Provider: "github",
|
||||
Settings: make(map[string]interface{}),
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
IsDeleted: false,
|
||||
Source: models.DB,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Provider: "okta",
|
||||
Settings: make(map[string]interface{}),
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
IsDeleted: false,
|
||||
Source: models.System,
|
||||
},
|
||||
}
|
||||
if tt.expectedServiceCall {
|
||||
service.On("ListWithRedactedSecrets", mock.AnythingOfType("*context.valueCtx")).Return(serviceResult, tt.errFromService).Once()
|
||||
}
|
||||
server := setupTests(t, service)
|
||||
|
||||
path := "/api/v1/sso-settings"
|
||||
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)
|
||||
|
||||
bodyBytes, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
var accessErrorResponse struct {
|
||||
AccessErrorID string `json:"accessErrorId"`
|
||||
Message string `json:"message"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
err = json.Unmarshal(bodyBytes, &accessErrorResponse)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal response body into accessErrorResponse: %v", err)
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectedErrMessage, accessErrorResponse.Message)
|
||||
return
|
||||
}
|
||||
|
||||
var actual []*models.SSOSettings
|
||||
err = json.Unmarshal(bodyBytes, &actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.ElementsMatch(t, tt.expectedResult, actual)
|
||||
err = res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getPermissionsForActionAndScope(action, scope string) map[int64]map[string][]string {
|
||||
return map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{{
|
||||
|
||||
Reference in New Issue
Block a user