diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 3424a210b..513ed28d1 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -469,3 +469,13 @@ func (s *Server) AddZitadelProvider(ctx context.Context, req *mgmt_pb.AddZitadel Details: object_pb.DomainToAddDetailsPb(details), }, nil } + +func (s *Server) UpdateZitadelProvider(ctx context.Context, req *mgmt_pb.UpdateZitadelProviderRequest) (*mgmt_pb.UpdateZitadelProviderResponse, error) { + details, err := s.command.UpdateOrgZitadelProvider(ctx, req.Id, authz.GetCtxData(ctx).OrgID, updateZitadelProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateZitadelProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index 2ad46df01..d311813f1 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -543,14 +543,6 @@ func signatureAlgorithmToCommand(signatureAlgorithm idp_pb.SAMLSignatureAlgorith } func addZitadelProviderToCommand(req *mgmt_pb.AddZitadelProviderRequest) command.ZitadelProvider { - instanceRolesInfo := make([]idp.RolesInfo, 0, len(req.InstanceRolesInfo)) - for _, info := range req.InstanceRolesInfo { - instanceRolesInfo = append(instanceRolesInfo, idp.RolesInfo{ - OrganizationID: info.OrganizationId, - OrganizationDomain: info.OrganizationDomain, - }) - } - return command.ZitadelProvider{ Name: req.Name, Issuer: req.Issuer, @@ -558,6 +550,32 @@ func addZitadelProviderToCommand(req *mgmt_pb.AddZitadelProviderRequest) command ClientSecret: req.ClientSecret, Scopes: req.Scopes, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), - InstanceRolesInfo: instanceRolesInfo, + InstanceRolesInfo: instanceRolesInfoToCommand(req.InstanceRolesInfo), } } + +func updateZitadelProviderToCommand(req *mgmt_pb.UpdateZitadelProviderRequest) command.ZitadelProvider { + return command.ZitadelProvider{ + Name: req.Name, + Issuer: req.Issuer, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + InstanceRolesInfo: instanceRolesInfoToCommand(req.InstanceRolesInfo), + } +} + +func instanceRolesInfoToCommand(reqInstanceRolesInfo []*idp_pb.InstanceRolesInfo) []idp.RolesInfo { + if reqInstanceRolesInfo == nil { + return nil + } + instanceRolesInfo := make([]idp.RolesInfo, 0, len(reqInstanceRolesInfo)) + for _, info := range reqInstanceRolesInfo { + instanceRolesInfo = append(instanceRolesInfo, idp.RolesInfo{ + OrganizationID: info.OrganizationId, + OrganizationDomain: info.OrganizationDomain, + }) + } + return instanceRolesInfo +} diff --git a/internal/api/grpc/management/idp_converter_test.go b/internal/api/grpc/management/idp_converter_test.go index dc4a38ce8..b27a09d48 100644 --- a/internal/api/grpc/management/idp_converter_test.go +++ b/internal/api/grpc/management/idp_converter_test.go @@ -233,7 +233,6 @@ func Test_addZitadelProviderToCommand(t *testing.T) { IsLinkingAllowed: false, IsAutoUpdate: true, }, - InstanceRolesInfo: []idp.RolesInfo{}, }, }, { @@ -294,3 +293,98 @@ func Test_addZitadelProviderToCommand(t *testing.T) { }) } } + +func Test_updateZitadelProviderToCommand(t *testing.T) { + t.Parallel() + tests := []struct { + name string + req *mgmt_pb.UpdateZitadelProviderRequest + want command.ZitadelProvider + }{ + { + name: "without instance roles info", + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Name: "Zitadel Support IdP", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"}, + ProviderOptions: &idp_pb.Options{ + IsLinkingAllowed: false, + IsCreationAllowed: true, + IsAutoCreation: false, + IsAutoUpdate: true, + AutoLinking: 0, + }, + }, + want: command.ZitadelProvider{ + Name: "Zitadel Support IdP", + ClientID: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsAutoCreation: false, + IsLinkingAllowed: false, + IsAutoUpdate: true, + }, + InstanceRolesInfo: nil, + }, + }, + { + name: "all fields filled", + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Name: "Zitadel Support IdP", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"}, + ProviderOptions: &idp_pb.Options{ + IsLinkingAllowed: false, + IsCreationAllowed: true, + IsAutoCreation: false, + IsAutoUpdate: true, + AutoLinking: 0, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + { + + OrganizationId: "org2", + OrganizationDomain: "org2.com", + }, + }, + }, + want: command.ZitadelProvider{ + Name: "Zitadel Support IdP", + ClientID: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsAutoCreation: false, + IsLinkingAllowed: false, + IsAutoUpdate: true, + }, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "org2.com", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := updateZitadelProviderToCommand(tt.req) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/management/integration_test/idp_test.go b/internal/api/grpc/management/integration_test/idp_test.go index 4a066a2d4..fdf42ffd5 100644 --- a/internal/api/grpc/management/integration_test/idp_test.go +++ b/internal/api/grpc/management/integration_test/idp_test.go @@ -231,3 +231,186 @@ func Test_AddZitadelProvider(t *testing.T) { }) } } + +func Test_UpdateZitadelProvider(t *testing.T) { + existingProvider, err := Instance.Client.Mgmt.AddZitadelProvider(OrgCTX, &mgmt_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, existingProvider.GetId()) + + type args struct { + ctx context.Context + req *mgmt_pb.UpdateZitadelProviderRequest + } + tests := []struct { + name string + args args + wantErr error + wantResponse *mgmt_pb.UpdateZitadelProviderResponse + }{ + { + name: "no permissions, error", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + }, + }, + }, + wantErr: status.Error(codes.NotFound, "membership not found (AUTHZ-cdgFk)"), + }, + { + name: "insufficient permissions, error", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeLogin), + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + Issuer: "acme.example.com", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + IsAutoCreation: false, + }, + }, + }, + wantErr: status.Error(codes.PermissionDenied, "No matching permissions found (AUTH-5mWD2)"), + }, + { + name: "missing required field: id", + args: args{ + ctx: OrgCTX, + req: &mgmt_pb.UpdateZitadelProviderRequest{}, + }, + wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.Id: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "missing required field: name", + args: args{ + ctx: OrgCTX, + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + }, + }, + wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.Name: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "missing required field: issuer", + args: args{ + ctx: OrgCTX, + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + Name: "Zitadel Support IdP updated", + }, + }, + wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.Issuer: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "missing required field: client_id", + args: args{ + ctx: OrgCTX, + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + Name: "Zitadel Support IdP updated", + Issuer: "acme.example.com", + }, + }, + wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.ClientId: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "update, ok", + args: args{ + ctx: OrgCTX, + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + Name: "Zitadel Support IdP updated", + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile", "openid", "offline_access"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + IsAutoCreation: false, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + { + OrganizationId: "org2", + OrganizationDomain: "org2.com", + }, + }, + }, + }, + wantResponse: &mgmt_pb.UpdateZitadelProviderResponse{ + Details: &object_pb.ObjectDetails{ + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + }, + { + name: "update with scopes unset, ok", + args: args{ + ctx: OrgCTX, + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + Name: "Zitadel Support IdP updated again", + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: []string{}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + IsAutoCreation: false, + }, + InstanceRolesInfo: nil, + }, + }, + wantResponse: &mgmt_pb.UpdateZitadelProviderResponse{ + Details: &object_pb.ObjectDetails{ + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + before := time.Now() + got, err := Client.UpdateZitadelProvider(tt.args.ctx, tt.args.req) + after := time.Now() + if tt.wantErr != nil { + require.Error(t, err) + grpcStatus, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, status.Code(tt.wantErr), grpcStatus.Code()) + assert.Equal(t, status.Convert(tt.wantErr).Message(), grpcStatus.Message()) + return + } + require.NoError(t, err) + assert.NotNil(t, got) + assert.WithinRange(t, got.GetDetails().GetChangeDate().AsTime(), before, after) + assert.Equal(t, tt.wantResponse.GetDetails().GetResourceOwner(), got.GetDetails().GetResourceOwner()) + + // todo: check if the update was actually applied by fetching the provider and checking its fields when the functionality is implemented + }) + } +} diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index 735d40e32..b41b16f8e 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -811,7 +811,7 @@ func (c *Commands) prepareMigrateOrgOIDCToAzureADProvider(a *org.Aggregate, writ return nil, err } if !writeModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "INST-Dg239201", "Errors.Instance.IDPConfig.NotExisting") + return nil, zerrors.ThrowNotFound(nil, "ORG-Dg239201", "Errors.Org.IDPConfig.NotExisting") } secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) if err != nil { @@ -853,7 +853,7 @@ func (c *Commands) prepareMigrateOrgOIDCToGoogleProvider(a *org.Aggregate, write return nil, err } if !writeModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "INST-x09981", "Errors.Instance.IDPConfig.NotExisting") + return nil, zerrors.ThrowNotFound(nil, "ORG-x09981", "Errors.Org.IDPConfig.NotExisting") } secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) if err != nil { @@ -1919,7 +1919,7 @@ func (c *Commands) AddOrgZitadelProvider(ctx context.Context, resourceOwner stri return "", nil, err } - err = c.validateOrgZitadelProvider(&provider) + err = c.validateOrgZitadelProvider(&provider, true) if err != nil { return "", nil, err } @@ -1970,7 +1970,71 @@ func (c *Commands) prepareAddOrgZitadelProvider(a *org.Aggregate, writeModel *Or } } -func (c *Commands) validateOrgZitadelProvider(provider *ZitadelProvider) error { +func (c *Commands) UpdateOrgZitadelProvider(ctx context.Context, id string, resourceOwner string, provider ZitadelProvider) (*domain.ObjectDetails, error) { + err := c.validateOrgZitadelProvider(&provider, false) + if err != nil { + return nil, err + } + writeModel := NewZitadelOrgIDPWriteModel(resourceOwner, id) + orgAgg := org.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgZitadelProvider(orgAgg, writeModel, provider)) //nolint:staticcheck + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) prepareUpdateOrgZitadelProvider(a *org.Aggregate, writeModel *OrgZitadelIDPWriteModel, provider ZitadelProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-3pxLbA", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "ORG-Bg281", "Errors.Org.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.Issuer, + provider.ClientID, + provider.ClientSecret, + c.idpConfigEncryption, + provider.Scopes, + provider.IDPOptions, + provider.InstanceRolesInfo, + ) + if err != nil || event == nil { + return nil, err + } + return []eventstore.Command{event}, nil + }, nil + } +} + +func (c *Commands) validateOrgZitadelProvider(provider *ZitadelProvider, create bool) error { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return zerrors.ThrowInvalidArgument(nil, "ORG-c3EC3W", "Errors.Invalid.Argument") } @@ -1980,10 +2044,11 @@ func (c *Commands) validateOrgZitadelProvider(provider *ZitadelProvider) error { if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { return zerrors.ThrowInvalidArgument(nil, "ORG-feyb3A", "Errors.Invalid.Argument") } - if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + // client_secret is required on creation, but optional on update + provider.ClientSecret = strings.TrimSpace(provider.ClientSecret) + if create && provider.ClientSecret == "" { return zerrors.ThrowInvalidArgument(nil, "ORG-DxaSb9", "Errors.Invalid.Argument") } - for i := range provider.InstanceRolesInfo { provider.InstanceRolesInfo[i].OrganizationID = strings.TrimSpace(provider.InstanceRolesInfo[i].OrganizationID) if provider.InstanceRolesInfo[i].OrganizationID == "" { diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index f8940f159..c0d9fc4ea 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -1070,6 +1070,8 @@ func (wm *OrgZitadelIDPWriteModel) AppendEvents(events ...eventstore.Event) { switch e := event.(type) { case *org.ZitadelIDPAddedEvent: wm.ZitadelIDPWriteModel.AppendEvents(&e.ZitadelIDPAddedEvent) + case *org.ZitadelIDPChangedEvent: + wm.ZitadelIDPWriteModel.AppendEvents(&e.ZitadelIDPChangedEvent) default: wm.ZitadelIDPWriteModel.AppendEvents(e) } @@ -1088,3 +1090,29 @@ func (wm *OrgZitadelIDPWriteModel) Query() *eventstore.SearchQueryBuilder { EventData(map[string]interface{}{"id": wm.ID}). Builder() } + +func (wm *OrgZitadelIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, name, issuer, clientID, clientSecretString string, + secretCrypto crypto.EncryptionAlgorithm, + scopes []string, + options idp.Options, + info []idp.RolesInfo, +) (*org.ZitadelIDPChangedEvent, error) { + + changes, err := wm.NewChanges( + name, + issuer, + clientID, + clientSecretString, + secretCrypto, + scopes, + options, + info, + ) + if err != nil || len(changes) == 0 { + return nil, err + } + return org.NewZitadelIDPChangedEvent(ctx, aggregate, id, changes), nil +} diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 5b23b052c..090d6868c 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -6307,7 +6307,512 @@ func TestCommandSide_AddOrgZitadelIDP(t *testing.T) { idGenerator: tt.fields.idGenerator, idpConfigEncryption: tt.fields.secretCrypto, } - id, got, err := c.AddOrgZitadelProvider(tt.args.ctx, tt.args.resourceOwner, tt.args.provider) + idpID, got, err := c.AddOrgZitadelProvider(tt.args.ctx, tt.args.resourceOwner, tt.args.provider) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.id, idpID) + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} + +func TestCommands_UpdateOrgZitadelIDP(t *testing.T) { + pushErr := errors.New("push error") + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + id string + resourceOwner string + provider ZitadelProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: expectEventstore(), + }, + args{ + id: "idp-id", + resourceOwner: "org1", + provider: ZitadelProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "ORG-c3EC3W", "")) + }, + }, + }, + { + "invalid issuer", + fields{ + eventstore: expectEventstore(), + }, + args{ + id: "idp-id", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "ORG-TYAbiK", "")) + }, + }, + }, + { + "invalid clientID", + fields{ + eventstore: expectEventstore(), + }, + args{ + id: "idp-id", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "ORG-feyb3A", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + id: "idp-id", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "ORG-Bg281", "Errors.Org.IDPConfig.NotExisting")) + }, + }, + }, + { + name: "push error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewZitadelIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{openid.ScopeOpenID, openid.ScopeEmail}, + idp.Options{}, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + }, + )), + ), + expectPushFailed( + pushErr, + org.NewZitadelIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.ZitadelIDPChanges{ + idp.ChangeZitadelIDPName("new name"), + idp.ChangeZitadelIDPIssuer("new issuer"), + idp.ChangeZitadelIDPClientID("clientID2"), + idp.ChangeZitadelIDPClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + }, + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + id: "id1", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "new name", + Issuer: "new issuer", + ClientID: "clientID2", + ClientSecret: "newSecret", + Scopes: nil, + InstanceRolesInfo: nil, + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, pushErr) + }, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewZitadelIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "idp-id", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "key-id", + Crypted: []byte("clientSecret"), + }, + []string{openid.ScopeOpenID, openid.ScopeEmail}, + idp.Options{}, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + ), + ), + ), + ), + }, + args: args{ + id: "idp-id", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewZitadelIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{openid.ScopeOpenID, openid.ScopeEmail}, + idp.Options{}, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + }, + )), + ), + expectPush( + func() eventstore.Command { + t := true + event := org.NewZitadelIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.ZitadelIDPChanges{ + idp.ChangeZitadelIDPName("new name"), + idp.ChangeZitadelIDPIssuer("new issuer"), + idp.ChangeZitadelIDPClientID("clientID2"), + idp.ChangeZitadelIDPClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + idp.ChangeZitadelIDPScopes([]string{"openid", "profile"}), + idp.ChangeZitadelIDPOptions(idp.OptionChanges{ + IsCreationAllowed: &t, + IsLinkingAllowed: &t, + IsAutoCreation: &t, + IsAutoUpdate: &t, + }), + idp.ChangeZitadelIDPInstanceRolesInfo([]idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }), + }, + ) + return event + }(), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + id: "id1", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "new name", + Issuer: "new issuer", + ClientID: "clientID2", + ClientSecret: "newSecret", + Scopes: []string{"openid", "profile"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "change, unset scopes, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewZitadelIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{openid.ScopeOpenID, openid.ScopeEmail}, + idp.Options{}, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + }, + )), + ), + expectPush( + func() eventstore.Command { + t := true + event := org.NewZitadelIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.ZitadelIDPChanges{ + idp.ChangeZitadelIDPName("new name"), + idp.ChangeZitadelIDPIssuer("new issuer"), + idp.ChangeZitadelIDPClientID("clientID2"), + idp.ChangeZitadelIDPClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + idp.ChangeZitadelIDPScopes(nil), + idp.ChangeZitadelIDPOptions(idp.OptionChanges{ + IsCreationAllowed: &t, + IsLinkingAllowed: &t, + IsAutoCreation: &t, + IsAutoUpdate: &t, + }), + idp.ChangeZitadelIDPInstanceRolesInfo([]idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }), + }, + ) + return event + }(), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + id: "id1", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "new name", + Issuer: "new issuer", + ClientID: "clientID2", + ClientSecret: "newSecret", + Scopes: []string{}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "change, unset instance roles info, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewZitadelIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{openid.ScopeOpenID, openid.ScopeEmail}, + idp.Options{}, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + }, + )), + ), + expectPush( + func() eventstore.Command { + t := true + event := org.NewZitadelIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.ZitadelIDPChanges{ + idp.ChangeZitadelIDPName("new name"), + idp.ChangeZitadelIDPIssuer("new issuer"), + idp.ChangeZitadelIDPClientID("clientID2"), + idp.ChangeZitadelIDPClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + idp.ChangeZitadelIDPOptions(idp.OptionChanges{ + IsCreationAllowed: &t, + IsLinkingAllowed: &t, + IsAutoCreation: &t, + IsAutoUpdate: &t, + }), + idp.ChangeZitadelIDPInstanceRolesInfo(nil), + }, + ) + return event + }(), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + id: "id1", + resourceOwner: "org1", + provider: ZitadelProvider{ + Name: "new name", + Issuer: "new issuer", + ClientID: "clientID2", + ClientSecret: "newSecret", + Scopes: nil, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + InstanceRolesInfo: []idp.RolesInfo{}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.UpdateOrgZitadelProvider(context.Background(), tt.args.id, tt.args.resourceOwner, tt.args.provider) if tt.res.err == nil { assert.NoError(t, err) } @@ -6315,7 +6820,6 @@ func TestCommandSide_AddOrgZitadelIDP(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.id, id) assertObjectDetails(t, tt.res.want, got) } }) diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index acee88ffd..7956bb199 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -704,6 +704,10 @@ func (p *idpTemplateProjection) Reducers() []handler.AggregateReducer { Event: org.ZitadelIDPAddedEventType, Reduce: p.reduceZitadelIDPAdded, }, + { + Event: org.ZitadelIDPChangedEventType, + Reduce: p.reduceZitadelIDPChanged, + }, { Event: org.IDPConfigRemovedEventType, Reduce: p.reduceIDPConfigRemoved, @@ -2640,8 +2644,10 @@ func (p *idpTemplateProjection) reduceZitadelIDPChanged(event eventstore.Event) switch e := event.(type) { case *instance.ZitadelIDPChangedEvent: idpEvent = e.ZitadelIDPChangedEvent + case *org.ZitadelIDPChangedEvent: + idpEvent = e.ZitadelIDPChangedEvent default: - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-qjMUfi", "reduce.wrong.event.type %v", []eventstore.EventType{instance.ZitadelIDPChangedEventType}) + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-qjMUfi", "reduce.wrong.event.type %v", []eventstore.EventType{instance.ZitadelIDPChangedEventType, org.ZitadelIDPChangedEventType}) } ops := make([]func(eventstore.Event) handler.Exec, 0, 2) diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index 519edd0cc..be491ff9c 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -4981,6 +4981,179 @@ func TestIDPTemplateProjection_reducesZitadel(t *testing.T) { }, }, }, + { + name: "org reduceZitadelIDPChanged minimal", + args: args{ + event: getEvent( + testEvent( + org.ZitadelIDPChangedEventType, + org.AggregateType, + []byte(`{ + "id": "idp-id", + "isCreationAllowed": true, + "clientId": "id" +}`), + ), eventstore.GenericEventMapper[org.ZitadelIDPChangedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateUpdateMinimalStmt, + expectedArgs: []interface{}{ + true, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates6_zitadel SET client_id = $1 WHERE (idp_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "id", + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "org reduceZitadelIDPChanged", + args: args{ + event: getEvent( + testEvent( + org.ZitadelIDPChangedEventType, + org.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "issuer": "issuer", + "clientId": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "instanceRolesInfo": [{ + "organizationId": "org3", + "organizationDomain": "org3.com" + }], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true, + "autoLinkingOption": 1 +}`), + ), eventstore.GenericEventMapper[org.ZitadelIDPChangedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateUpdateStmt, + expectedArgs: []interface{}{ + "name", + true, + true, + true, + true, + domain.AutoLinkingOptionUsername, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates6_zitadel SET (client_id, client_secret, issuer, scopes, instance_roles_info) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)", + expectedArgs: []interface{}{ + "client_id", + anyArg{}, + "issuer", + database.TextArray[string]{"profile"}, + []byte(`[{"organizationId":"org3","organizationDomain":"org3.com"}]`), + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "org reduceZitadelIDPChanged - unset scopes and instance roles info", + args: args{ + event: getEvent( + testEvent( + org.ZitadelIDPChangedEventType, + org.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "issuer": "issuer", + "clientId": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": [], + "instanceRolesInfo": [], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true, + "autoLinkingOption": 1 +}`), + ), eventstore.GenericEventMapper[org.ZitadelIDPChangedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateUpdateStmt, + expectedArgs: []interface{}{ + "name", + true, + true, + true, + true, + domain.AutoLinkingOptionUsername, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates6_zitadel SET (client_id, client_secret, issuer, scopes, instance_roles_info) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)", + expectedArgs: []interface{}{ + "client_id", + anyArg{}, + "issuer", + database.TextArray[string]{}, + []byte(`[]`), + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 83bd48a83..6ab02bd51 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -104,6 +104,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SAMLIDPAddedEventType, SAMLIDPAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, SAMLIDPChangedEventType, SAMLIDPChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, ZitadelIDPAddedEventType, eventstore.GenericEventMapper[ZitadelIDPAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, ZitadelIDPChangedEventType, eventstore.GenericEventMapper[ZitadelIDPChangedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, TriggerActionsSetEventType, TriggerActionsSetEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper) diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index bb884f047..e7f076676 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -39,6 +39,7 @@ const ( SAMLIDPChangedEventType eventstore.EventType = "org.idp.saml.changed" IDPRemovedEventType eventstore.EventType = "org.idp.removed" ZitadelIDPAddedEventType eventstore.EventType = "org.idp.zitadel.added" + ZitadelIDPChangedEventType eventstore.EventType = "org.idp.zitadel.changed" ) type OAuthIDPAddedEvent struct { @@ -1171,3 +1172,29 @@ func NewZitadelIDPAddedEvent( func (e *ZitadelIDPAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { e.BaseEvent = *event } + +type ZitadelIDPChangedEvent struct { + idp.ZitadelIDPChangedEvent +} + +func NewZitadelIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.ZitadelIDPChanges, +) *ZitadelIDPChangedEvent { + changedEvent := idp.NewZitadelIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + ZitadelIDPChangedEventType, + ), + id, + changes, + ) + return &ZitadelIDPChangedEvent{ZitadelIDPChangedEvent: *changedEvent} +} + +func (e *ZitadelIDPChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +}