From 97f79e0aae47b8a767c5b49f2316cc164231fdfa Mon Sep 17 00:00:00 2001 From: icerzack Date: Wed, 25 Feb 2026 12:01:56 +0300 Subject: [PATCH 1/7] commit changes --- service/federations/saml/requests.go | 255 +++++++++ service/federations/saml/requests_test.go | 487 +++++++++++++++++- service/federations/saml/schemas.go | 32 ++ service/federations/saml/testdata/fixtures.go | 36 +- 4 files changed, 806 insertions(+), 4 deletions(-) diff --git a/service/federations/saml/requests.go b/service/federations/saml/requests.go index 22c34ee..463ed97 100644 --- a/service/federations/saml/requests.go +++ b/service/federations/saml/requests.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "net/http" "net/url" @@ -82,6 +83,34 @@ func (s *Service) Get(ctx context.Context, federationID string) (*GetResponse, e return &federation, nil } +// Exists checks that Federation with federationID exists. +func (s *Service) Exists(ctx context.Context, federationID string) (bool, error) { + if federationID == "" { + return false, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + } + + path, err := url.JoinPath(apiVersion, "federations", "saml", federationID) + if err != nil { + return false, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ + Body: nil, + 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 false, err + } + + return true, nil +} + // Create creates a new Federation. func (s *Service) Create(ctx context.Context, input CreateRequest) (*CreateResponse, error) { if input.Name == "" { @@ -189,3 +218,229 @@ func (s *Service) Delete(ctx context.Context, federationID string) error { return nil } + +// Preview returns preview information of Federation using federationID or alias. +func (s *Service) Preview(ctx context.Context, federationID string) (*FederationPreview, error) { + if federationID == "" { + return nil, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + } + + path, err := url.JoinPath(apiVersion, "federations", "saml", federationID, "preview") + 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 preview FederationPreview + err = client.UnmarshalJSON(response, &preview) + if err != nil { + return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + return &preview, nil +} + +// GetGroupMappings returns a list of mappings for the Federation. +func (s *Service) GetGroupMappings(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 +} + +// UpdateGroupMappings updates mappings for the Federation. +func (s *Service) UpdateGroupMappings( + ctx context.Context, federationID string, input GroupMappingsRequest, +) (*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()} + } + + body, err := json.Marshal(input) + if err != nil { + return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + response, 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 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 +} + +// AddExternalGroupMapping creates mapping between internal and external group. +func (s *Service) AddExternalGroupMapping( + ctx context.Context, federationID, groupID, externalGroupID 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()} + } + + _, 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 +} + +// DeleteExternalGroupMapping deletes mapping between internal and external group. +func (s *Service) DeleteExternalGroupMapping( + ctx context.Context, federationID, groupID, externalGroupID 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()} + } + + _, 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 +} + +// ExternalGroupMappingExists checks that internal and external groups are mapped. +func (s *Service) ExternalGroupMappingExists( + ctx context.Context, federationID, groupID, externalGroupID string, +) (bool, error) { + if federationID == "" { + return false, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + } + if groupID == "" { + return false, iamerrors.Error{Err: iamerrors.ErrGroupIDRequired, Desc: "No groupID was provided."} + } + if externalGroupID == "" { + return false, 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 false, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + _, 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 +} diff --git a/service/federations/saml/requests_test.go b/service/federations/saml/requests_test.go index dde79b1..325d2c5 100644 --- a/service/federations/saml/requests_test.go +++ b/service/federations/saml/requests_test.go @@ -15,8 +15,10 @@ import ( ) const ( - federationsURL = "v1/federations/saml" - federationsIDURL = "v1/federations/saml/123" + federationsURL = "v1/federations/saml" + federationsIDURL = "v1/federations/saml/123" + federationGroupMappingsURL = "v1/federations/saml/123/group-mappings" + federationExternalGroupMappingURL = "v1/federations/saml/123/group-mappings/456/external-groups/external-group" ) // Convenience vars for bool values. @@ -119,11 +121,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 +205,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 +288,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 +400,475 @@ 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) + }) + } +} + +func TestGetGroupMappings(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) + + 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.GetGroupMappings(ctx, "123") + + require.ErrorIs(err, tt.expectedError) + assert.Equal(tt.expectedResponse, actual) + }) + } +} + +func TestUpdateGroupMappings(t *testing.T) { + tests := []struct { + name string + prepare func() + input GroupMappingsRequest + expectedResponse *GroupMappingsResponse + expectedError error + }{ + { + name: "ok", + prepare: func() { + httpmock.RegisterResponder( + http.MethodPut, testdata.TestURL+federationGroupMappingsURL, + func(r *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusOK, testdata.TestGroupMappingsResponse) + return resp, nil + }) + }, + input: GroupMappingsRequest{ + GroupMappings: []GroupMapping{ + { + InternalGroupID: "456", + ExternalGroupID: "external-group", + }, + }, + }, + expectedResponse: &GroupMappingsResponse{ + 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", + }, + }, + }, + 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.UpdateGroupMappings(ctx, "123", tt.input) + + require.ErrorIs(err, tt.expectedError) + assert.Equal(tt.expectedResponse, actual) + }) + } +} + +func TestAddExternalGroupMapping(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) + + 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() + err := federationsAPI.AddExternalGroupMapping(ctx, "123", "456", "external-group") + + require.ErrorIs(err, tt.expectedError) + }) + } +} + +func TestDeleteExternalGroupMapping(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) + + 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() + err := federationsAPI.DeleteExternalGroupMapping(ctx, "123", "456", "external-group") + + require.ErrorIs(err, tt.expectedError) + }) + } +} + +func TestExternalGroupMappingExists(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) + + 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.ExternalGroupMappingExists(ctx, "123", "456", "external-group") + + require.ErrorIs(err, tt.expectedError) + assert.Equal(tt.expectedExists, exists) + }) + } +} diff --git a/service/federations/saml/schemas.go b/service/federations/saml/schemas.go index 5df3115..8af5748 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"` } // ListResponse represents all federations in account. @@ -37,15 +40,44 @@ 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"` } // 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"` +} + +// 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"` +} + +// 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 UpdateGroupMappings 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/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" +}` From 485fad664986b13b565d263fee4f84125397beb6 Mon Sep 17 00:00:00 2001 From: icerzack Date: Wed, 25 Feb 2026 13:42:39 +0300 Subject: [PATCH 2/7] lint fix --- service/federations/saml/requests.go | 52 ++++++++++++---------------- service/federations/saml/schemas.go | 6 ++-- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/service/federations/saml/requests.go b/service/federations/saml/requests.go index 463ed97..a845e97 100644 --- a/service/federations/saml/requests.go +++ b/service/federations/saml/requests.go @@ -219,15 +219,18 @@ func (s *Service) Delete(ctx context.Context, federationID string) error { return nil } -// Preview returns preview information of Federation using federationID or alias. -func (s *Service) Preview(ctx context.Context, federationID string) (*FederationPreview, error) { +func (s *Service) getFederationResource( + ctx context.Context, federationID string, segments []string, output interface{}, +) error { if federationID == "" { - return nil, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + return iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} } - path, err := url.JoinPath(apiVersion, "federations", "saml", federationID, "preview") + pathSegments := append([]string{apiVersion, "federations", "saml", federationID}, segments...) + + path, err := url.JoinPath(pathSegments[0], pathSegments[1:]...) if err != nil { - return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} } response, err := s.baseClient.DoRequest(ctx, client.DoRequestInput{ @@ -237,43 +240,34 @@ func (s *Service) Preview(ctx context.Context, federationID string) (*Federation }) if err != nil { //nolint:wrapcheck // DoRequest already wraps the error. - return nil, err + return err } - var preview FederationPreview - err = client.UnmarshalJSON(response, &preview) + err = client.UnmarshalJSON(response, output) if err != nil { - return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} } - return &preview, nil + return nil } -// GetGroupMappings returns a list of mappings for the Federation. -func (s *Service) GetGroupMappings(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, - }) +// 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 { - //nolint:wrapcheck // DoRequest already wraps the error. return nil, err } + return &preview, nil +} + +// GetGroupMappings returns a list of mappings for the Federation. +func (s *Service) GetGroupMappings(ctx context.Context, federationID string) (*GroupMappingsResponse, error) { var mappings GroupMappingsResponse - err = client.UnmarshalJSON(response, &mappings) + err := s.getFederationResource(ctx, federationID, []string{"group-mappings"}, &mappings) if err != nil { - return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return nil, err } return &mappings, nil diff --git a/service/federations/saml/schemas.go b/service/federations/saml/schemas.go index 8af5748..9414ad9 100644 --- a/service/federations/saml/schemas.go +++ b/service/federations/saml/schemas.go @@ -13,7 +13,7 @@ type Federation struct { ForceAuthn bool `json:"force_authn"` SessionMaxAgeHours int `json:"session_max_age_hours"` AutoUsersCreation bool `json:"auto_users_creation"` - EnableGroupMapping bool `json:"enable_group_mappings"` + EnableGroupMapping bool `json:"enable_group_mappings"` //nolint:tagliatelle } // ListResponse represents all federations in account. @@ -41,7 +41,7 @@ type CreateRequest struct { 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"` + EnableGroupMapping bool `json:"enable_group_mappings"` //nolint:tagliatelle } // UpdateRequest is used to set options for Update method. @@ -55,7 +55,7 @@ type UpdateRequest struct { 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"` + EnableGroupMapping *bool `json:"enable_group_mappings,omitempty"` //nolint:tagliatelle } // FederationPreview represents preview information about Federation. From 08cd6eb4d8044962f96fbe0c876f126a1965800e Mon Sep 17 00:00:00 2001 From: icerzack Date: Wed, 25 Feb 2026 16:47:35 +0300 Subject: [PATCH 3/7] fix --- service/federations/saml/requests.go | 73 +++++++++------------------- 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/service/federations/saml/requests.go b/service/federations/saml/requests.go index a845e97..ac62977 100644 --- a/service/federations/saml/requests.go +++ b/service/federations/saml/requests.go @@ -310,18 +310,17 @@ func (s *Service) UpdateGroupMappings( return &mappings, nil } -// AddExternalGroupMapping creates mapping between internal and external group. -func (s *Service) AddExternalGroupMapping( - ctx context.Context, federationID, groupID, externalGroupID string, -) error { +func buildExternalGroupMappingPath( + federationID, groupID, externalGroupID string, +) (string, error) { if federationID == "" { - return iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} + return "", iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} } if groupID == "" { - return iamerrors.Error{Err: iamerrors.ErrGroupIDRequired, Desc: "No groupID was provided."} + return "", iamerrors.Error{Err: iamerrors.ErrGroupIDRequired, Desc: "No groupID was provided."} } if externalGroupID == "" { - return iamerrors.Error{Err: iamerrors.ErrInputDataRequired, Desc: "No externalGroupID was provided."} + return "", iamerrors.Error{Err: iamerrors.ErrInputDataRequired, Desc: "No externalGroupID was provided."} } path, err := url.JoinPath( @@ -335,7 +334,19 @@ func (s *Service) AddExternalGroupMapping( externalGroupID, ) if err != nil { - return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return "", iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + } + + return path, nil +} + +// AddExternalGroupMapping creates mapping between internal and external group. +func (s *Service) AddExternalGroupMapping( + 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{ @@ -355,28 +366,9 @@ func (s *Service) AddExternalGroupMapping( func (s *Service) DeleteExternalGroupMapping( ctx context.Context, federationID, groupID, externalGroupID 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, - ) + path, err := buildExternalGroupMappingPath(federationID, groupID, externalGroupID) if err != nil { - return iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return err } _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ @@ -396,28 +388,9 @@ func (s *Service) DeleteExternalGroupMapping( func (s *Service) ExternalGroupMappingExists( ctx context.Context, federationID, groupID, externalGroupID string, ) (bool, error) { - if federationID == "" { - return false, iamerrors.Error{Err: iamerrors.ErrFederationIDRequired, Desc: "No federationID was provided."} - } - if groupID == "" { - return false, iamerrors.Error{Err: iamerrors.ErrGroupIDRequired, Desc: "No groupID was provided."} - } - if externalGroupID == "" { - return false, iamerrors.Error{Err: iamerrors.ErrInputDataRequired, Desc: "No externalGroupID was provided."} - } - - path, err := url.JoinPath( - apiVersion, - "federations", - "saml", - federationID, - "group-mappings", - groupID, - "external-groups", - externalGroupID, - ) + path, err := buildExternalGroupMappingPath(federationID, groupID, externalGroupID) if err != nil { - return false, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return false, err } _, err = s.baseClient.DoRequest(ctx, client.DoRequestInput{ From 9bc5ebb7a32801a9d1a74c559490a16a23525c5b Mon Sep 17 00:00:00 2001 From: icerzack Date: Thu, 26 Feb 2026 16:20:58 +0300 Subject: [PATCH 4/7] fix: incorrect http method --- service/federations/saml/requests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/federations/saml/requests.go b/service/federations/saml/requests.go index ac62977..438a2af 100644 --- a/service/federations/saml/requests.go +++ b/service/federations/saml/requests.go @@ -293,7 +293,7 @@ func (s *Service) UpdateGroupMappings( response, err := s.baseClient.DoRequest(ctx, client.DoRequestInput{ Body: bytes.NewReader(body), - Method: http.MethodPut, + Method: http.MethodPatch, Path: path, }) if err != nil { From 927147c3803bc2808a66bd43a08d8e1d62d6371a Mon Sep 17 00:00:00 2001 From: icerzack Date: Thu, 26 Feb 2026 16:22:51 +0300 Subject: [PATCH 5/7] test: fix tests --- service/federations/saml/requests_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/federations/saml/requests_test.go b/service/federations/saml/requests_test.go index 325d2c5..7d36a3c 100644 --- a/service/federations/saml/requests_test.go +++ b/service/federations/saml/requests_test.go @@ -618,7 +618,7 @@ func TestUpdateGroupMappings(t *testing.T) { name: "ok", prepare: func() { httpmock.RegisterResponder( - http.MethodPut, testdata.TestURL+federationGroupMappingsURL, + http.MethodPatch, testdata.TestURL+federationGroupMappingsURL, func(r *http.Request) (*http.Response, error) { resp := httpmock.NewStringResponse(http.StatusOK, testdata.TestGroupMappingsResponse) return resp, nil @@ -646,7 +646,7 @@ func TestUpdateGroupMappings(t *testing.T) { name: "error", prepare: func() { httpmock.RegisterResponder( - http.MethodPut, testdata.TestURL+federationGroupMappingsURL, + http.MethodPatch, testdata.TestURL+federationGroupMappingsURL, func(r *http.Request) (*http.Response, error) { resp := httpmock.NewStringResponse(http.StatusForbidden, testdata.TestDoRequestErr) return resp, nil From 77c95091ebe9b9290068598263f55a9c5ddbb921 Mon Sep 17 00:00:00 2001 From: icerzack Date: Thu, 26 Feb 2026 16:56:52 +0300 Subject: [PATCH 6/7] refactor: move to external package --- service/federations/saml/groupmappings/doc.go | 3 + .../saml/groupmappings/requests.go | 190 ++++++++++ .../saml/groupmappings/requests_test.go | 343 ++++++++++++++++++ .../federations/saml/groupmappings/schemas.go | 18 + service/federations/saml/requests.go | 206 +---------- service/federations/saml/requests_test.go | 340 +---------------- service/federations/saml/schemas.go | 16 - 7 files changed, 576 insertions(+), 540 deletions(-) create mode 100644 service/federations/saml/groupmappings/doc.go create mode 100644 service/federations/saml/groupmappings/requests.go create mode 100644 service/federations/saml/groupmappings/requests_test.go create mode 100644 service/federations/saml/groupmappings/schemas.go 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..86e07a7 --- /dev/null +++ b/service/federations/saml/groupmappings/requests_test.go @@ -0,0 +1,343 @@ +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..f670f1a --- /dev/null +++ b/service/federations/saml/groupmappings/schemas.go @@ -0,0 +1,18 @@ +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 438a2af..0b08918 100644 --- a/service/federations/saml/requests.go +++ b/service/federations/saml/requests.go @@ -11,21 +11,24 @@ import ( "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, } } @@ -56,29 +59,10 @@ 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) { - if federationID == "" { - return nil, 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()} - } - - 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 federation GetResponse - err = client.UnmarshalJSON(response, &federation) + err := s.getFederationResource(ctx, federationID, nil, &federation) if err != nil { - return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} + return nil, err } return &federation, nil } @@ -111,6 +95,17 @@ func (s *Service) Exists(ctx context.Context, federationID string) (bool, error) 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, err + } + + return &preview, nil +} + // Create creates a new Federation. func (s *Service) Create(ctx context.Context, input CreateRequest) (*CreateResponse, error) { if input.Name == "" { @@ -250,164 +245,3 @@ func (s *Service) getFederationResource( return 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, err - } - - return &preview, nil -} - -// GetGroupMappings returns a list of mappings for the Federation. -func (s *Service) GetGroupMappings(ctx context.Context, federationID string) (*GroupMappingsResponse, error) { - var mappings GroupMappingsResponse - err := s.getFederationResource(ctx, federationID, []string{"group-mappings"}, &mappings) - if err != nil { - return nil, err - } - - return &mappings, nil -} - -// UpdateGroupMappings updates mappings for the Federation. -func (s *Service) UpdateGroupMappings( - ctx context.Context, federationID string, input GroupMappingsRequest, -) (*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()} - } - - body, err := json.Marshal(input) - if err != nil { - return nil, iamerrors.Error{Err: iamerrors.ErrInternalAppError, Desc: err.Error()} - } - - response, err := s.baseClient.DoRequest(ctx, client.DoRequestInput{ - Body: bytes.NewReader(body), - Method: http.MethodPatch, - 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 -} - -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 -} - -// AddExternalGroupMapping creates mapping between internal and external group. -func (s *Service) AddExternalGroupMapping( - 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 -} - -// DeleteExternalGroupMapping deletes mapping between internal and external group. -func (s *Service) DeleteExternalGroupMapping( - 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 -} - -// ExternalGroupMappingExists checks that internal and external groups are mapped. -func (s *Service) ExternalGroupMappingExists( - 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 -} diff --git a/service/federations/saml/requests_test.go b/service/federations/saml/requests_test.go index 7d36a3c..b5d0ae0 100644 --- a/service/federations/saml/requests_test.go +++ b/service/federations/saml/requests_test.go @@ -15,10 +15,8 @@ import ( ) const ( - federationsURL = "v1/federations/saml" - federationsIDURL = "v1/federations/saml/123" - federationGroupMappingsURL = "v1/federations/saml/123/group-mappings" - federationExternalGroupMappingURL = "v1/federations/saml/123/group-mappings/456/external-groups/external-group" + federationsURL = "v1/federations/saml" + federationsIDURL = "v1/federations/saml/123" ) // Convenience vars for bool values. @@ -538,337 +536,3 @@ func TestPreview(t *testing.T) { }) } } - -func TestGetGroupMappings(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) - - 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.GetGroupMappings(ctx, "123") - - require.ErrorIs(err, tt.expectedError) - assert.Equal(tt.expectedResponse, actual) - }) - } -} - -func TestUpdateGroupMappings(t *testing.T) { - tests := []struct { - name string - prepare func() - input GroupMappingsRequest - expectedResponse *GroupMappingsResponse - expectedError error - }{ - { - name: "ok", - prepare: func() { - httpmock.RegisterResponder( - http.MethodPatch, testdata.TestURL+federationGroupMappingsURL, - func(r *http.Request) (*http.Response, error) { - resp := httpmock.NewStringResponse(http.StatusOK, testdata.TestGroupMappingsResponse) - return resp, nil - }) - }, - input: GroupMappingsRequest{ - GroupMappings: []GroupMapping{ - { - InternalGroupID: "456", - ExternalGroupID: "external-group", - }, - }, - }, - expectedResponse: &GroupMappingsResponse{ - GroupMappings: []GroupMapping{ - { - InternalGroupID: "456", - ExternalGroupID: "external-group", - }, - }, - }, - expectedError: nil, - }, - { - name: "error", - prepare: func() { - httpmock.RegisterResponder( - http.MethodPatch, 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", - }, - }, - }, - 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.UpdateGroupMappings(ctx, "123", tt.input) - - require.ErrorIs(err, tt.expectedError) - assert.Equal(tt.expectedResponse, actual) - }) - } -} - -func TestAddExternalGroupMapping(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) - - 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() - err := federationsAPI.AddExternalGroupMapping(ctx, "123", "456", "external-group") - - require.ErrorIs(err, tt.expectedError) - }) - } -} - -func TestDeleteExternalGroupMapping(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) - - 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() - err := federationsAPI.DeleteExternalGroupMapping(ctx, "123", "456", "external-group") - - require.ErrorIs(err, tt.expectedError) - }) - } -} - -func TestExternalGroupMappingExists(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) - - 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.ExternalGroupMappingExists(ctx, "123", "456", "external-group") - - require.ErrorIs(err, tt.expectedError) - assert.Equal(tt.expectedExists, exists) - }) - } -} diff --git a/service/federations/saml/schemas.go b/service/federations/saml/schemas.go index 9414ad9..72ca3ec 100644 --- a/service/federations/saml/schemas.go +++ b/service/federations/saml/schemas.go @@ -65,19 +65,3 @@ type FederationPreview struct { Description string `json:"description"` Alias string `json:"alias"` } - -// 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 UpdateGroupMappings 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"` -} From d8091dc491a02f667c9db01d75808df3469090ba Mon Sep 17 00:00:00 2001 From: icerzack Date: Thu, 26 Feb 2026 17:03:56 +0300 Subject: [PATCH 7/7] style: lint fix --- service/federations/saml/groupmappings/requests_test.go | 5 ++--- service/federations/saml/groupmappings/schemas.go | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/service/federations/saml/groupmappings/requests_test.go b/service/federations/saml/groupmappings/requests_test.go index 86e07a7..2f06ab9 100644 --- a/service/federations/saml/groupmappings/requests_test.go +++ b/service/federations/saml/groupmappings/requests_test.go @@ -15,8 +15,8 @@ import ( ) const ( - federationGroupMappingsURL = "v1/federations/saml/123/group-mappings" - federationExternalGroupMappingURL = "v1/federations/saml/123/group-mappings/456/external-groups/external-group" + federationGroupMappingsURL = "v1/federations/saml/123/group-mappings" + federationExternalGroupMappingURL = "v1/federations/saml/123/group-mappings/456/external-groups/external-group" ) func TestList(t *testing.T) { @@ -340,4 +340,3 @@ func TestExists(t *testing.T) { }) } } - diff --git a/service/federations/saml/groupmappings/schemas.go b/service/federations/saml/groupmappings/schemas.go index f670f1a..b72f200 100644 --- a/service/federations/saml/groupmappings/schemas.go +++ b/service/federations/saml/groupmappings/schemas.go @@ -15,4 +15,3 @@ type GroupMappingsRequest struct { type GroupMappingsResponse struct { GroupMappings []GroupMapping `json:"group_mappings"` } -