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:
colin-stuart
2024-01-19 14:39:09 -05:00
committed by GitHub
co-authored by Mihaly Gyongyosi
parent 40312c527b
commit e241188f00
9 changed files with 579 additions and 29 deletions
+169
View File
@@ -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{{