diff --git a/service/federations/saml/groupmappings/doc.go b/service/federations/saml/groupmappings/doc.go new file mode 100644 index 0000000..19cbc13 --- /dev/null +++ b/service/federations/saml/groupmappings/doc.go @@ -0,0 +1,3 @@ +// Package groupmappings provides a set of functions for interacting with +// the Selectel SAML Federation Group Mappings API. +package groupmappings diff --git a/service/federations/saml/groupmappings/requests.go b/service/federations/saml/groupmappings/requests.go new file mode 100644 index 0000000..8a2e16c --- /dev/null +++ b/service/federations/saml/groupmappings/requests.go @@ -0,0 +1,190 @@ +package groupmappings + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "net/url" + + "github.com/selectel/iam-go/iamerrors" + "github.com/selectel/iam-go/internal/client" +) + +const apiVersion = "v1" + +// Service is used to communicate with the Federations Group Mappings API. +type Service struct { + baseClient *client.BaseClient +} + +// New Initialises Service with the given client. +func New(baseClient *client.BaseClient) *Service { + return &Service{ + baseClient: baseClient, + } +} + +// List returns a list of mappings for the Federation. +func (s *Service) List(ctx context.Context, federationID string) (*GroupMappingsResponse, error) { + if federationID == "" { + return nil, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + } + + path, err := url.JoinPath(apiVersion, "federations", "saml", federationID, "group-mappings") + if err != nil { + return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + response, err := s.baseClient.DoRequest(ctx, client.DoRequestInput{ + Body: nil, + Method: http.MethodGet, + Path: path, + }) + if err != nil { + //nolint:wrapcheck // DoRequest already wraps the error. + return nil, err + } + + var mappings GroupMappingsResponse + err = client.UnmarshalJSON(response, &mappings) + if err != nil { + return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + return &mappings, nil +} + +// Update updates mappings for the Federation. +func (s *Service) Update( + ctx context.Context, federationID string, input GroupMappingsRequest, +) error { + if federationID == "" { + return iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + } + + path, err := url.JoinPath(apiVersion, "federations", "saml", federationID, "group-mappings") + if err != nil { + return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + body, err := json.Marshal(input) + if err != nil { + return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ + Body: bytes.NewReader(body), + Method: http.MethodPut, + Path: path, + }) + if err != nil { + //nolint:wrapcheck // DoRequest already wraps the error. + return err + } + + return nil +} + +// Add creates mapping between internal and external group. +func (s *Service) Add( + ctx context.Context, federationID, groupID, externalGroupID string, +) error { + path, err := buildExternalGroupMappingPath(federationID, groupID, externalGroupID) + if err != nil { + return err + } + + _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ + Body: nil, + Method: http.MethodPut, + Path: path, + }) + if err != nil { + //nolint:wrapcheck // DoRequest already wraps the error. + return err + } + + return nil +} + +// Delete deletes mapping between internal and external group. +func (s *Service) Delete( + ctx context.Context, federationID, groupID, externalGroupID string, +) error { + path, err := buildExternalGroupMappingPath(federationID, groupID, externalGroupID) + if err != nil { + return err + } + + _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ + Body: nil, + Method: http.MethodDelete, + Path: path, + }) + if err != nil { + //nolint:wrapcheck // DoRequest already wraps the error. + return err + } + + return nil +} + +// Exists checks that internal and external groups are mapped. +func (s *Service) Exists( + ctx context.Context, federationID, groupID, externalGroupID string, +) (bool, error) { + path, err := buildExternalGroupMappingPath(federationID, groupID, externalGroupID) + if err != nil { + return false, err + } + + _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ + Body: nil, + Method: http.MethodHead, + Path: path, + }) + if err != nil { + if errors.Is(err, iamerrors.ErrFederationNotFound) || + errors.Is(err, iamerrors.ErrGroupNotFound) || + errors.Is(err, iamerrors.ErrUserOrGroupNotFound) { + return false, nil + } + + //nolint:wrapcheck // DoRequest already wraps the error. + return false, err + } + + return true, nil +} + +func buildExternalGroupMappingPath( + federationID, groupID, externalGroupID string, +) (string, error) { + if federationID == "" { + return "", iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + } + if groupID == "" { + return "", iamerrors.Error{Err: iamerrors.ErrGroupIDRequired, Desc: "No groupID was provided."} + } + if externalGroupID == "" { + return "", iamerrors.Error{Err: iamerrors.ErrInputDataRequired, Desc: "No externalGroupID was provided."} + } + + path, err := url.JoinPath( + apiVersion, + "federations", + "saml", + federationID, + "group-mappings", + groupID, + "external-groups", + externalGroupID, + ) + if err != nil { + return "", iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + return path, nil +} diff --git a/service/federations/saml/groupmappings/requests_test.go b/service/federations/saml/groupmappings/requests_test.go new file mode 100644 index 0000000..2f06ab9 --- /dev/null +++ b/service/federations/saml/groupmappings/requests_test.go @@ -0,0 +1,342 @@ +package groupmappings + +import ( + "context" + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/selectel/iam-go/iamerrors" + "github.com/selectel/iam-go/internal/client" + "github.com/selectel/iam-go/service/federations/saml/testdata" +) + +const ( + federationGroupMappingsURL = "v1/federations/saml/123/group-mappings" + federationExternalGroupMappingURL = "v1/federations/saml/123/group-mappings/456/external-groups/external-group" +) + +func TestList(t *testing.T) { + tests := []struct { + name string + prepare func() + expectedResponse *GroupMappingsResponse + expectedError error + }{ + { + name: "ok", + prepare: func() { + httpmock.RegisterResponder( + http.MethodGet, testdata.TestURL+federationGroupMappingsURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusOK, testdata.TestGroupMappingsResponse) + return resp, nil + }) + }, + expectedResponse: &GroupMappingsResponse{ + GroupMappings: []GroupMapping{ + { + InternalGroupID: "456", + ExternalGroupID: "external-group", + }, + }, + }, + expectedError: nil, + }, + { + name: "error", + prepare: func() { + httpmock.RegisterResponder( + http.MethodGet, testdata.TestURL+federationGroupMappingsURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) + return resp, nil + }) + }, + expectedResponse: nil, + expectedError: iamerrors.ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + api := New(&client.BaseClient{ + HTTPClient: &http.Client{}, + APIUrl: testdata.TestURL, + AuthMethod: &client.KeystoneTokenAuth{KeystoneToken: testdata.TestToken}, + }) + + httpmock.ActivateNonDefault(api.baseClient.HTTPClient) + defer httpmock.DeactivateAndReset() + + tt.prepare() + + ctx := context.Background() + actual, err := api.List(ctx, "123") + + require.ErrorIs(err, tt.expectedError) + assert.Equal(tt.expectedResponse, actual) + }) + } +} + +func TestUpdate(t *testing.T) { + tests := []struct { + name string + prepare func() + input GroupMappingsRequest + expectedError error + }{ + { + name: "ok", + prepare: func() { + httpmock.RegisterResponder( + http.MethodPut, testdata.TestURL+federationGroupMappingsURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusNoContent, "") + return resp, nil + }) + }, + input: GroupMappingsRequest{ + GroupMappings: []GroupMapping{ + { + InternalGroupID: "456", + ExternalGroupID: "external-group", + }, + }, + }, + expectedError: nil, + }, + { + name: "error", + prepare: func() { + httpmock.RegisterResponder( + http.MethodPut, testdata.TestURL+federationGroupMappingsURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) + return resp, nil + }) + }, + input: GroupMappingsRequest{ + GroupMappings: []GroupMapping{ + { + InternalGroupID: "456", + ExternalGroupID: "external-group", + }, + }, + }, + expectedError: iamerrors.ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + api := New(&client.BaseClient{ + HTTPClient: &http.Client{}, + APIUrl: testdata.TestURL, + AuthMethod: &client.KeystoneTokenAuth{KeystoneToken: testdata.TestToken}, + }) + + httpmock.ActivateNonDefault(api.baseClient.HTTPClient) + defer httpmock.DeactivateAndReset() + + tt.prepare() + + ctx := context.Background() + err := api.Update(ctx, "123", tt.input) + + require.ErrorIs(err, tt.expectedError) + }) + } +} + +func TestAdd(t *testing.T) { + tests := []struct { + name string + prepare func() + expectedError error + }{ + { + name: "ok", + prepare: func() { + httpmock.RegisterResponder( + http.MethodPut, testdata.TestURL+federationExternalGroupMappingURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusNoContent, "") + return resp, nil + }) + }, + expectedError: nil, + }, + { + name: "error", + prepare: func() { + httpmock.RegisterResponder( + http.MethodPut, testdata.TestURL+federationExternalGroupMappingURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) + return resp, nil + }) + }, + expectedError: iamerrors.ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + api := New(&client.BaseClient{ + HTTPClient: &http.Client{}, + APIUrl: testdata.TestURL, + AuthMethod: &client.KeystoneTokenAuth{KeystoneToken: testdata.TestToken}, + }) + + httpmock.ActivateNonDefault(api.baseClient.HTTPClient) + defer httpmock.DeactivateAndReset() + + tt.prepare() + + ctx := context.Background() + err := api.Add(ctx, "123", "456", "external-group") + + require.ErrorIs(err, tt.expectedError) + }) + } +} + +func TestDelete(t *testing.T) { + tests := []struct { + name string + prepare func() + expectedError error + }{ + { + name: "ok", + prepare: func() { + httpmock.RegisterResponder( + http.MethodDelete, testdata.TestURL+federationExternalGroupMappingURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusNoContent, "") + return resp, nil + }) + }, + expectedError: nil, + }, + { + name: "error", + prepare: func() { + httpmock.RegisterResponder( + http.MethodDelete, testdata.TestURL+federationExternalGroupMappingURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) + return resp, nil + }) + }, + expectedError: iamerrors.ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + api := New(&client.BaseClient{ + HTTPClient: &http.Client{}, + APIUrl: testdata.TestURL, + AuthMethod: &client.KeystoneTokenAuth{KeystoneToken: testdata.TestToken}, + }) + + httpmock.ActivateNonDefault(api.baseClient.HTTPClient) + defer httpmock.DeactivateAndReset() + + tt.prepare() + + ctx := context.Background() + err := api.Delete(ctx, "123", "456", "external-group") + + require.ErrorIs(err, tt.expectedError) + }) + } +} + +func TestExists(t *testing.T) { + tests := []struct { + name string + prepare func() + expectedExists bool + expectedError error + }{ + { + name: "exists", + prepare: func() { + httpmock.RegisterResponder( + http.MethodHead, testdata.TestURL+federationExternalGroupMappingURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusNoContent, "") + return resp, nil + }) + }, + expectedExists: true, + expectedError: nil, + }, + { + name: "not found", + prepare: func() { + httpmock.RegisterResponder( + http.MethodHead, testdata.TestURL+federationExternalGroupMappingURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusNotFound, testdata.TestUserOrGroupNotFoundErr) + return resp, nil + }) + }, + expectedExists: false, + expectedError: nil, + }, + { + name: "error", + prepare: func() { + httpmock.RegisterResponder( + http.MethodHead, testdata.TestURL+federationExternalGroupMappingURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) + return resp, nil + }) + }, + expectedExists: false, + expectedError: iamerrors.ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + api := New(&client.BaseClient{ + HTTPClient: &http.Client{}, + APIUrl: testdata.TestURL, + AuthMethod: &client.KeystoneTokenAuth{KeystoneToken: testdata.TestToken}, + }) + + httpmock.ActivateNonDefault(api.baseClient.HTTPClient) + defer httpmock.DeactivateAndReset() + + tt.prepare() + + ctx := context.Background() + exists, err := api.Exists(ctx, "123", "456", "external-group") + + require.ErrorIs(err, tt.expectedError) + assert.Equal(tt.expectedExists, exists) + }) + } +} diff --git a/service/federations/saml/groupmappings/schemas.go b/service/federations/saml/groupmappings/schemas.go new file mode 100644 index 0000000..b72f200 --- /dev/null +++ b/service/federations/saml/groupmappings/schemas.go @@ -0,0 +1,17 @@ +package groupmappings + +// GroupMapping represents mapping between internal and external group. +type GroupMapping struct { + InternalGroupID string `json:"internal_group_id"` + ExternalGroupID string `json:"external_group_id"` +} + +// GroupMappingsRequest is used to set options for Update method. +type GroupMappingsRequest struct { + GroupMappings []GroupMapping `json:"group_mappings"` +} + +// GroupMappingsResponse represents all mappings for the specified Federation. +type GroupMappingsResponse struct { + GroupMappings []GroupMapping `json:"group_mappings"` +} diff --git a/service/federations/saml/requests.go b/service/federations/saml/requests.go index 22c34ee..0b08918 100644 --- a/service/federations/saml/requests.go +++ b/service/federations/saml/requests.go @@ -4,27 +4,31 @@ import ( "bytes" "context" "encoding/json" + "errors" "net/http" "net/url" "github.com/selectel/iam-go/iamerrors" "github.com/selectel/iam-go/internal/client" "github.com/selectel/iam-go/service/federations/saml/certificates" + "github.com/selectel/iam-go/service/federations/saml/groupmappings" ) const apiVersion = "v1" // Service is used to communicate with the Federations API. type Service struct { - Certificates *certificates.Service - baseClient *client.BaseClient + Certificates *certificates.Service + GroupMappings *groupmappings.Service + baseClient *client.BaseClient } // New Initialises Service with the given client. func New(baseClient *client.BaseClient) *Service { return &Service{ - Certificates: certificates.New(baseClient), - baseClient: baseClient, + Certificates: certificates.New(baseClient), + GroupMappings: groupmappings.New(baseClient), + baseClient: baseClient, } } @@ -55,31 +59,51 @@ func (s *Service) List(ctx context.Context) (*ListResponse, error) { // Get returns an info of Federation with federationID. func (s *Service) Get(ctx context.Context, federationID string) (*GetResponse, error) { + var federation GetResponse + err := s.getFederationResource(ctx, federationID, nil, &federation) + if err != nil { + return nil, err + } + return &federation, nil +} + +// Exists checks that Federation with federationID exists. +func (s *Service) Exists(ctx context.Context, federationID string) (bool, error) { if federationID == "" { - return nil, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + return false, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} } path, err := url.JoinPath(apiVersion, "federations", "saml", federationID) if err != nil { - return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return false, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} } - response, err := s.baseClient.DoRequest(ctx, client.DoRequestInput{ + _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ Body: nil, - Method: http.MethodGet, + Method: http.MethodHead, Path: path, }) if err != nil { + if errors.Is(err, iamerrors.ErrFederationNotFound) { + return false, nil + } + //nolint:wrapcheck // DoRequest already wraps the error. - return nil, err + return false, err } - var federation GetResponse - err = client.UnmarshalJSON(response, &federation) + return true, nil +} + +// Preview returns preview information of Federation using federationID or alias. +func (s *Service) Preview(ctx context.Context, federationID string) (*FederationPreview, error) { + var preview FederationPreview + err := s.getFederationResource(ctx, federationID, []string{"preview"}, &preview) if err != nil { - return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return nil, err } - return &federation, nil + + return &preview, nil } // Create creates a new Federation. @@ -189,3 +213,35 @@ func (s *Service) Delete(ctx context.Context, federationID string) error { return nil } + +func (s *Service) getFederationResource( + ctx context.Context, federationID string, segments []string, output interface{}, +) error { + if federationID == "" { + return iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + } + + pathSegments := append([]string{apiVersion, "federations", "saml", federationID}, segments...) + + path, err := url.JoinPath(pathSegments[0], pathSegments[1:]...) + if err != nil { + return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + response, err := s.baseClient.DoRequest(ctx, client.DoRequestInput{ + Body: nil, + Method: http.MethodGet, + Path: path, + }) + if err != nil { + //nolint:wrapcheck // DoRequest already wraps the error. + return err + } + + err = client.UnmarshalJSON(response, output) + if err != nil { + return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + return nil +} diff --git a/service/federations/saml/requests_test.go b/service/federations/saml/requests_test.go index dde79b1..b5d0ae0 100644 --- a/service/federations/saml/requests_test.go +++ b/service/federations/saml/requests_test.go @@ -119,11 +119,14 @@ func TestGet(t *testing.T) { AccountID: "123", Name: "test_name", Description: "test_description", + Alias: "test_alias", Issuer: "test_issuer", SSOUrl: "test_sso_url", SignAuthnRequests: true, ForceAuthn: true, SessionMaxAgeHours: 1, + AutoUsersCreation: true, + EnableGroupMapping: true, }, }, expectedError: nil, @@ -200,11 +203,14 @@ func TestCreate(t *testing.T) { AccountID: "123", Name: "test_name", Description: "test_description", + Alias: "test_alias", Issuer: "test_issuer", SSOUrl: "test_sso_url", SignAuthnRequests: true, ForceAuthn: true, SessionMaxAgeHours: 1, + AutoUsersCreation: true, + EnableGroupMapping: true, }, }, expectedError: nil, @@ -280,11 +286,14 @@ func TestUpdate(t *testing.T) { AccountID: "123", Name: "test_name", Description: "test_description", + Alias: "test_alias", Issuer: "test_issuer", SSOUrl: "test_sso_url", SignAuthnRequests: true, ForceAuthn: true, SessionMaxAgeHours: 1, + AutoUsersCreation: true, + EnableGroupMapping: true, }, }, expectedError: nil, @@ -389,3 +398,141 @@ func TestDelete(t *testing.T) { }) } } + +func TestExists(t *testing.T) { + tests := []struct { + name string + prepare func() + expectedExists bool + expectedError error + }{ + { + name: "exists", + prepare: func() { + httpmock.RegisterResponder( + http.MethodHead, testdata.TestURL+federationsIDURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusOK, "") + return resp, nil + }) + }, + expectedExists: true, + expectedError: nil, + }, + { + name: "not found", + prepare: func() { + httpmock.RegisterResponder( + http.MethodHead, testdata.TestURL+federationsIDURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusNotFound, testdata.TestFederationNotFoundErr) + return resp, nil + }) + }, + expectedExists: false, + expectedError: nil, + }, + { + name: "error", + prepare: func() { + httpmock.RegisterResponder( + http.MethodHead, testdata.TestURL+federationsIDURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) + return resp, nil + }) + }, + expectedExists: false, + expectedError: iamerrors.ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + federationsAPI := New(&client.BaseClient{ + HTTPClient: &http.Client{}, + APIUrl: testdata.TestURL, + AuthMethod: &client.KeystoneTokenAuth{KeystoneToken: testdata.TestToken}, + }) + + httpmock.ActivateNonDefault(federationsAPI.baseClient.HTTPClient) + defer httpmock.DeactivateAndReset() + + tt.prepare() + + ctx := context.Background() + exists, err := federationsAPI.Exists(ctx, "123") + + require.ErrorIs(err, tt.expectedError) + assert.Equal(tt.expectedExists, exists) + }) + } +} + +func TestPreview(t *testing.T) { + tests := []struct { + name string + prepare func() + expectedResponse *FederationPreview + expectedError error + }{ + { + name: "ok", + prepare: func() { + httpmock.RegisterResponder( + http.MethodGet, testdata.TestURL+federationsIDURL+"/preview", + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusOK, testdata.TestPreviewFederationResponse) + return resp, nil + }) + }, + expectedResponse: &FederationPreview{ + ID: "123", + Name: "test_name", + Description: "test_description", + Alias: "test_alias", + }, + expectedError: nil, + }, + { + name: "error", + prepare: func() { + httpmock.RegisterResponder( + http.MethodGet, testdata.TestURL+federationsIDURL+"/preview", + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) + return resp, nil + }) + }, + expectedResponse: nil, + expectedError: iamerrors.ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + federationsAPI := New(&client.BaseClient{ + HTTPClient: &http.Client{}, + APIUrl: testdata.TestURL, + AuthMethod: &client.KeystoneTokenAuth{KeystoneToken: testdata.TestToken}, + }) + + httpmock.ActivateNonDefault(federationsAPI.baseClient.HTTPClient) + defer httpmock.DeactivateAndReset() + + tt.prepare() + + ctx := context.Background() + actual, err := federationsAPI.Preview(ctx, "123") + + require.ErrorIs(err, tt.expectedError) + assert.Equal(tt.expectedResponse, actual) + }) + } +} diff --git a/service/federations/saml/schemas.go b/service/federations/saml/schemas.go index 5df3115..72ca3ec 100644 --- a/service/federations/saml/schemas.go +++ b/service/federations/saml/schemas.go @@ -6,11 +6,14 @@ type Federation struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` + Alias string `json:"alias"` Issuer string `json:"issuer"` SSOUrl string `json:"sso_url"` SignAuthnRequests bool `json:"sign_authn_requests"` ForceAuthn bool `json:"force_authn"` SessionMaxAgeHours int `json:"session_max_age_hours"` + AutoUsersCreation bool `json:"auto_users_creation"` + EnableGroupMapping bool `json:"enable_group_mappings"` //nolint:tagliatelle } // ListResponse represents all federations in account. @@ -37,15 +40,28 @@ type CreateRequest struct { SignAuthnRequests bool `json:"sign_authn_requests,omitempty"` ForceAuthn bool `json:"force_authn,omitempty"` SessionMaxAgeHours int `json:"session_max_age_hours"` + AutoUsersCreation bool `json:"auto_users_creation"` + EnableGroupMapping bool `json:"enable_group_mappings"` //nolint:tagliatelle } // UpdateRequest is used to set options for Update method. type UpdateRequest struct { Name string `json:"name,omitempty"` Description *string `json:"description,omitempty"` + Alias string `json:"alias,omitempty"` Issuer string `json:"issuer,omitempty"` SSOUrl string `json:"sso_url,omitempty"` SignAuthnRequests *bool `json:"sign_authn_requests,omitempty"` ForceAuthn *bool `json:"force_authn,omitempty"` SessionMaxAgeHours int `json:"session_max_age_hours,omitempty"` + AutoUsersCreation *bool `json:"auto_users_creation,omitempty"` + EnableGroupMapping *bool `json:"enable_group_mappings,omitempty"` //nolint:tagliatelle +} + +// FederationPreview represents preview information about Federation. +type FederationPreview struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Alias string `json:"alias"` } diff --git a/service/federations/saml/testdata/fixtures.go b/service/federations/saml/testdata/fixtures.go index 944c708..3830832 100644 --- a/service/federations/saml/testdata/fixtures.go +++ b/service/federations/saml/testdata/fixtures.go @@ -26,11 +26,14 @@ const TestGetFederationResponse = `{ "account_id": "123", "name": "test_name", "description": "test_description", + "alias": "test_alias", "issuer": "test_issuer", "sso_url": "test_sso_url", "sign_authn_requests": true, "force_authn": true, - "session_max_age_hours": 1 + "session_max_age_hours": 1, + "auto_users_creation": true, + "enable_group_mappings": true }` const TestCreateFederationResponse = `{ @@ -38,14 +41,43 @@ const TestCreateFederationResponse = `{ "account_id": "123", "name": "test_name", "description": "test_description", + "alias": "test_alias", "issuer": "test_issuer", "sso_url": "test_sso_url", "sign_authn_requests": true, "force_authn": true, - "session_max_age_hours": 1 + "session_max_age_hours": 1, + "auto_users_creation": true, + "enable_group_mappings": true }` const TestDoRequestErr = `{ "code": "REQUEST_FORBIDDEN", "message": "You don't have permission to do this" }` + +const TestFederationNotFoundErr = `{ + "code": "FEDERATION_NOT_FOUND", + "message": "Federation not found" +}` + +const TestPreviewFederationResponse = `{ + "id": "123", + "name": "test_name", + "description": "test_description", + "alias": "test_alias" +}` + +const TestGroupMappingsResponse = `{ + "group_mappings": [ + { + "internal_group_id": "456", + "external_group_id": "external-group" + } + ] +}` + +const TestUserOrGroupNotFoundErr = `{ + "code": "USER_OR_GROUP_NOT_FOUND", + "message": "User or group not found" +}`