From 653a968d916d5f8c6634cab6ea97ead7f55beab3 Mon Sep 17 00:00:00 2001 From: Gayathri Vijayan <66356931+grvijayan@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:18:35 +0200 Subject: [PATCH] feat: implement instance-level UpdateZitadelProvider (#12378) # Which Problems Are Solved This PR implements the instance-level `UpdateZitadelProvider` endpoint, enabling updates to existing instance Zitadel IdP templates. # How the Problems Are Solved - Added `AdminService.UpdateZitadelProvider` handler, converter - Added the command-layer implementation to update a ZitadelProvider and push `instance.idp.zitadel.changed` event - Added projection-layer implementation to persist Zitadel provider updates into `projections.idp_templates6_zitadel` - Added unit and integration tests # Additional Changes N/A # Additional Context Related to https://github.com/zitadel/zitadel/issues/11922 Follow-up for PRs: - https://github.com/zitadel/zitadel/pull/12018 - https://github.com/zitadel/zitadel/pull/12020 - https://github.com/zitadel/zitadel/pull/12055 - https://github.com/zitadel/zitadel/pull/12056 - https://github.com/zitadel/zitadel/pull/12371 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- internal/api/grpc/admin/idp.go | 10 + internal/api/grpc/admin/idp_converter.go | 36 +- internal/api/grpc/admin/idp_converter_test.go | 97 +++- .../grpc/admin/integration_test/idp_test.go | 183 +++++++ internal/command/idp_model.go | 63 ++- internal/command/instance_idp.go | 73 ++- internal/command/instance_idp_model.go | 28 + internal/command/instance_idp_test.go | 504 ++++++++++++++++++ internal/query/projection/idp_template.go | 63 +++ .../query/projection/idp_template_test.go | 173 ++++++ internal/repository/idp/zitadel.go | 85 ++- internal/repository/instance/eventstore.go | 1 + internal/repository/instance/idp.go | 27 + 13 files changed, 1327 insertions(+), 16 deletions(-) diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index 1ba6fc203..f5ccfa457 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -477,3 +477,13 @@ func (s *Server) AddZitadelProvider(ctx context.Context, req *admin_pb.AddZitade Details: object_pb.DomainToAddDetailsPb(details), }, nil } + +func (s *Server) UpdateZitadelProvider(ctx context.Context, req *admin_pb.UpdateZitadelProviderRequest) (*admin_pb.UpdateZitadelProviderResponse, error) { + details, err := s.command.UpdateInstanceZitadelProvider(ctx, req.Id, updateZitadelProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateZitadelProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 1f1a104a6..48913ed0b 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -550,14 +550,6 @@ func signatureAlgorithmToCommand(signatureAlgorithm idp_pb.SAMLSignatureAlgorith } func addZitadelProviderToCommand(req *admin_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, @@ -565,6 +557,32 @@ func addZitadelProviderToCommand(req *admin_pb.AddZitadelProviderRequest) comman ClientSecret: req.ClientSecret, Scopes: req.Scopes, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), - InstanceRolesInfo: instanceRolesInfo, + InstanceRolesInfo: instanceRolesInfoToCommand(req.InstanceRolesInfo), } } + +func updateZitadelProviderToCommand(req *admin_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/admin/idp_converter_test.go b/internal/api/grpc/admin/idp_converter_test.go index b18ee650a..0c108f9ee 100644 --- a/internal/api/grpc/admin/idp_converter_test.go +++ b/internal/api/grpc/admin/idp_converter_test.go @@ -233,7 +233,7 @@ func Test_addZitadelProviderToCommand(t *testing.T) { IsLinkingAllowed: false, IsAutoUpdate: true, }, - InstanceRolesInfo: []idp.RolesInfo{}, + InstanceRolesInfo: nil, }, }, { @@ -294,3 +294,98 @@ func Test_addZitadelProviderToCommand(t *testing.T) { }) } } + +func Test_updateZitadelProviderToCommand(t *testing.T) { + t.Parallel() + tests := []struct { + name string + req *admin_pb.UpdateZitadelProviderRequest + want command.ZitadelProvider + }{ + { + name: "without instance roles info", + req: &admin_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: &admin_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/admin/integration_test/idp_test.go b/internal/api/grpc/admin/integration_test/idp_test.go index 7198e7d0e..e16f1b1b3 100644 --- a/internal/api/grpc/admin/integration_test/idp_test.go +++ b/internal/api/grpc/admin/integration_test/idp_test.go @@ -225,3 +225,186 @@ func Test_AddZitadelProvider(t *testing.T) { }) } } + +func Test_UpdateZitadelProvider(t *testing.T) { + existingProvider, err := Instance.Client.Admin.AddZitadelProvider(AdminCTX, &admin_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 *admin_pb.UpdateZitadelProviderRequest + } + tests := []struct { + name string + args args + wantErr error + wantResponse *admin_pb.UpdateZitadelProviderResponse + }{ + { + name: "no permissions, error", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &admin_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: &admin_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: AdminCTX, + req: &admin_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: AdminCTX, + req: &admin_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: AdminCTX, + req: &admin_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: AdminCTX, + req: &admin_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: AdminCTX, + req: &admin_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: &admin_pb.UpdateZitadelProviderResponse{ + Details: &object_pb.ObjectDetails{ + ResourceOwner: Instance.Instance.Id, + }, + }, + }, + { + name: "update with scopes unset, ok", + args: args{ + ctx: AdminCTX, + req: &admin_pb.UpdateZitadelProviderRequest{ + Id: existingProvider.GetId(), + Name: "Zitadel Support IdP updated again", + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: nil, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + IsAutoCreation: false, + }, + InstanceRolesInfo: nil, + }, + }, + wantResponse: &admin_pb.UpdateZitadelProviderResponse{ + Details: &object_pb.ObjectDetails{ + ResourceOwner: Instance.Instance.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/idp_model.go b/internal/command/idp_model.go index a3630cf46..0998fe3b0 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -2393,7 +2393,6 @@ func (wm *ZitadelIDPWriteModel) Reduce() error { case *idp.ZitadelIDPAddedEvent: wm.reduceAddedEvent(e) case *idp.ZitadelIDPChangedEvent: - // todo (@grvijayan): to please the linter for now wm.reduceChangedEvent(e) } } @@ -2412,5 +2411,65 @@ func (wm *ZitadelIDPWriteModel) reduceAddedEvent(e *idp.ZitadelIDPAddedEvent) { } func (wm *ZitadelIDPWriteModel) reduceChangedEvent(e *idp.ZitadelIDPChangedEvent) { - // todo (@grvijayan): will be implemented along with UpdateZitadelProvider changes + if e.ClientID != nil { + wm.ClientID = *e.ClientID + } + if e.ClientSecret != nil { + wm.ClientSecret = e.ClientSecret + } + if e.Name != nil { + wm.Name = *e.Name + } + if e.Issuer != nil { + wm.Issuer = *e.Issuer + } + if e.Scopes != nil { + wm.Scopes = *e.Scopes + } + if e.InstanceRolesInfo != nil { + wm.InstanceRolesInfo = *e.InstanceRolesInfo + } + wm.ReduceChanges(e.OptionChanges) +} + +func (wm *ZitadelIDPWriteModel) NewChanges( + name, + issuer, + clientID, + clientSecretString string, + secretCrypto crypto.EncryptionAlgorithm, + scopes []string, + options idp.Options, + info []idp.RolesInfo, +) ([]idp.ZitadelIDPChanges, error) { + changes := make([]idp.ZitadelIDPChanges, 0) + var clientSecret *crypto.CryptoValue + var err error + if clientSecretString != "" { + clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto) + if err != nil { + return nil, err + } + changes = append(changes, idp.ChangeZitadelIDPClientSecret(clientSecret)) + } + if wm.ClientID != clientID { + changes = append(changes, idp.ChangeZitadelIDPClientID(clientID)) + } + if wm.Name != name { + changes = append(changes, idp.ChangeZitadelIDPName(name)) + } + if wm.Issuer != issuer { + changes = append(changes, idp.ChangeZitadelIDPIssuer(issuer)) + } + if scopes != nil && !slices.Equal(wm.Scopes, scopes) { + changes = append(changes, idp.ChangeZitadelIDPScopes(scopes)) + } + if info != nil && !slices.Equal(wm.InstanceRolesInfo, info) { + changes = append(changes, idp.ChangeZitadelIDPInstanceRolesInfo(info)) + } + opts := wm.Changes(options) + if !opts.IsZero() { + changes = append(changes, idp.ChangeZitadelIDPOptions(opts)) + } + return changes, nil } diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 2e8c37856..84302e1ef 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -1948,7 +1948,7 @@ func (c *Commands) AddInstanceZitadelProvider(ctx context.Context, provider Zita return "", nil, err } - err = c.validateInstanceZitadelProvider(&provider) + err = c.validateInstanceZitadelProvider(&provider, true) if err != nil { return "", nil, err } @@ -1998,7 +1998,72 @@ func (c *Commands) prepareAddInstanceZitadelProvider(a *instance.Aggregate, writ } } -func (c *Commands) validateInstanceZitadelProvider(provider *ZitadelProvider) error { +func (c *Commands) UpdateInstanceZitadelProvider(ctx context.Context, id string, provider ZitadelProvider) (*domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + err := c.validateInstanceZitadelProvider(&provider, false) + if err != nil { + return nil, err + } + writeModel := NewInstanceZitadelIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceZitadelProvider(instanceAgg, 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) prepareUpdateInstanceZitadelProvider(a *instance.Aggregate, writeModel *InstanceZitadelIDPWriteModel, provider ZitadelProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "INST-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, "INST-Bg281", "Errors.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) validateInstanceZitadelProvider(provider *ZitadelProvider, create bool) error { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return zerrors.ThrowInvalidArgument(nil, "INST-Sgtj5", "Errors.Invalid.Argument") } @@ -2008,7 +2073,9 @@ func (c *Commands) validateInstanceZitadelProvider(provider *ZitadelProvider) er if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { return zerrors.ThrowInvalidArgument(nil, "INST-fb5jm", "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, "INST-Sfdf4", "Errors.Invalid.Argument") } diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index 5857ae46f..5271212f3 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -1058,6 +1058,8 @@ func (wm *InstanceZitadelIDPWriteModel) AppendEvents(events ...eventstore.Event) switch e := event.(type) { case *instance.ZitadelIDPAddedEvent: wm.ZitadelIDPWriteModel.AppendEvents(&e.ZitadelIDPAddedEvent) + case *instance.ZitadelIDPChangedEvent: + wm.ZitadelIDPWriteModel.AppendEvents(&e.ZitadelIDPChangedEvent) default: wm.ZitadelIDPWriteModel.AppendEvents(e) } @@ -1076,3 +1078,29 @@ func (wm *InstanceZitadelIDPWriteModel) Query() *eventstore.SearchQueryBuilder { EventData(map[string]interface{}{"id": wm.ID}). Builder() } + +func (wm *InstanceZitadelIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, name, issuer, clientID, clientSecretString string, + secretCrypto crypto.EncryptionAlgorithm, + scopes []string, + options idp.Options, + info []idp.RolesInfo, +) (*instance.ZitadelIDPChangedEvent, error) { + + changes, err := wm.NewChanges( + name, + issuer, + clientID, + clientSecretString, + secretCrypto, + scopes, + options, + info, + ) + if err != nil || len(changes) == 0 { + return nil, err + } + return instance.NewZitadelIDPChangedEvent(ctx, aggregate, id, changes), nil +} diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index f8eb31d11..61c011631 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -6286,3 +6286,507 @@ func TestCommandSide_AddInstanceZitadelIDP(t *testing.T) { }) } } + +func TestCommands_UpdateInstanceZitadelProvider(t *testing.T) { + pushErr := errors.New("push error") + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + id 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{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "idp-id", + provider: ZitadelProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Sgtj5", "")) + }, + }, + }, + { + "invalid issuer", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "idp-id", + provider: ZitadelProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Hz6zj", "")) + }, + }, + }, + { + "invalid clientID", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "idp-id", + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-fb5jm", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "idp-id", + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "INST-Bg281", "Errors.IDPConfig.NotExisting")) + }, + }, + }, + { + name: "push error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").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, + instance.NewZitadelIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").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{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + 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( + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").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{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "idp-id", + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").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 := instance.NewZitadelIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").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{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + 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: "instance1"}, + }, + }, + { + name: "change, unset scopes, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").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 := instance.NewZitadelIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").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{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + 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: "instance1"}, + }, + }, + { + name: "change, unset instance roles info, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").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 := instance.NewZitadelIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").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{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + 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: "instance1"}, + }, + }, + } + 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.UpdateInstanceZitadelProvider(tt.args.ctx, tt.args.id, 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 { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index b564ee19f..acee88ffd 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -551,6 +551,10 @@ func (p *idpTemplateProjection) Reducers() []handler.AggregateReducer { Event: instance.ZitadelIDPAddedEventType, Reduce: p.reduceZitadelIDPAdded, }, + { + Event: instance.ZitadelIDPChangedEventType, + Reduce: p.reduceZitadelIDPChanged, + }, { Event: instance.IDPConfigRemovedEventType, Reduce: p.reduceIDPConfigRemoved, @@ -2630,3 +2634,62 @@ func (p *idpTemplateProjection) reduceZitadelIDPAdded(event eventstore.Event) (* ), ), nil } + +func (p *idpTemplateProjection) reduceZitadelIDPChanged(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.ZitadelIDPChangedEvent + switch e := event.(type) { + case *instance.ZitadelIDPChangedEvent: + idpEvent = e.ZitadelIDPChangedEvent + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-qjMUfi", "reduce.wrong.event.type %v", []eventstore.EventType{instance.ZitadelIDPChangedEventType}) + } + + ops := make([]func(eventstore.Event) handler.Exec, 0, 2) + ops = append(ops, + handler.AddUpdateStatement( + reduceIDPChangedTemplateColumns(idpEvent.Name, idpEvent.CreationDate(), idpEvent.Sequence(), idpEvent.OptionChanges), + []handler.Condition{ + handler.NewCond(IDPTemplateIDCol, idpEvent.ID), + handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + ), + ) + zitadelIDPChangedColumns := reduceZitadelIDPChangedColumns(idpEvent) + if len(zitadelIDPChangedColumns) > 0 { + ops = append(ops, + handler.AddUpdateStatement( + zitadelIDPChangedColumns, + []handler.Condition{ + handler.NewCond(ZitadelIDCol, idpEvent.ID), + handler.NewCond(ZitadelInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + handler.WithTableSuffix(IDPTemplateZitadelSuffix), + ), + ) + } + + return handler.NewMultiStatement( + &idpEvent, + ops..., + ), nil +} + +func reduceZitadelIDPChangedColumns(idpEvent idp.ZitadelIDPChangedEvent) []handler.Column { + zitadelCols := make([]handler.Column, 0, 5) + if idpEvent.ClientID != nil { + zitadelCols = append(zitadelCols, handler.NewCol(ZitadelClientIDCol, *idpEvent.ClientID)) + } + if idpEvent.ClientSecret != nil { + zitadelCols = append(zitadelCols, handler.NewCol(ZitadelClientSecretCol, *idpEvent.ClientSecret)) + } + if idpEvent.Issuer != nil { + zitadelCols = append(zitadelCols, handler.NewCol(ZitadelIssuerCol, *idpEvent.Issuer)) + } + if idpEvent.Scopes != nil { + zitadelCols = append(zitadelCols, handler.NewCol(ZitadelScopesCol, database.TextArray[string](*idpEvent.Scopes))) + } + if idpEvent.InstanceRolesInfo != nil { + zitadelCols = append(zitadelCols, handler.NewJSONCol(ZitadelInstanceRolesInfoCol, *idpEvent.InstanceRolesInfo)) + } + return zitadelCols +} diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index b7a4d0233..519edd0cc 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -4808,6 +4808,179 @@ func TestIDPTemplateProjection_reducesZitadel(t *testing.T) { }, }, }, + { + name: "instance reduceZitadelIDPChanged minimal", + args: args{ + event: getEvent( + testEvent( + instance.ZitadelIDPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "isCreationAllowed": true, + "clientId": "id" +}`), + ), eventstore.GenericEventMapper[instance.ZitadelIDPChangedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + 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: "instance reduceZitadelIDPChanged", + args: args{ + event: getEvent( + testEvent( + instance.ZitadelIDPChangedEventType, + instance.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[instance.ZitadelIDPChangedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + 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: "instance reduceZitadelIDPChanged - unset scopes and instance roles info", + args: args{ + event: getEvent( + testEvent( + instance.ZitadelIDPChangedEventType, + instance.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[instance.ZitadelIDPChangedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + 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/idp/zitadel.go b/internal/repository/idp/zitadel.go index c222fbbde..77cc051d6 100644 --- a/internal/repository/idp/zitadel.go +++ b/internal/repository/idp/zitadel.go @@ -57,5 +57,88 @@ func (e *ZitadelIDPAddedEvent) UniqueConstraints() []*eventstore.UniqueConstrain type ZitadelIDPChangedEvent struct { eventstore.BaseEvent `json:"-"` - // todo (@grvijayan): will be added along with UpdateZitadelProvider changes + + ID string `json:"id"` + Name *string `json:"name,omitempty"` + Issuer *string `json:"issuer,omitempty"` + ClientID *string `json:"clientId,omitempty"` + ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` + Scopes *[]string `json:"scopes,omitempty"` + InstanceRolesInfo *[]RolesInfo `json:"instanceRolesInfo,omitempty"` + OptionChanges +} + +func NewZitadelIDPChangedEvent( + base *eventstore.BaseEvent, + id string, + changes []ZitadelIDPChanges, +) *ZitadelIDPChangedEvent { + e := &ZitadelIDPChangedEvent{ + BaseEvent: *base, + ID: id, + } + for _, change := range changes { + change(e) + } + return e +} + +type ZitadelIDPChanges func(*ZitadelIDPChangedEvent) + +func ChangeZitadelIDPName(name string) ZitadelIDPChanges { + return func(e *ZitadelIDPChangedEvent) { + e.Name = &name + } +} + +func ChangeZitadelIDPIssuer(issuer string) ZitadelIDPChanges { + return func(e *ZitadelIDPChangedEvent) { + e.Issuer = &issuer + } +} + +func ChangeZitadelIDPClientID(clientID string) ZitadelIDPChanges { + return func(e *ZitadelIDPChangedEvent) { + e.ClientID = &clientID + } +} + +func ChangeZitadelIDPClientSecret(clientSecret *crypto.CryptoValue) ZitadelIDPChanges { + return func(e *ZitadelIDPChangedEvent) { + e.ClientSecret = clientSecret + } +} + +func ChangeZitadelIDPScopes(scopes []string) ZitadelIDPChanges { + return func(e *ZitadelIDPChangedEvent) { + // explicitly set them to empty in case the scopes are unset + if scopes == nil { + scopes = make([]string, 0) + } + e.Scopes = &scopes + } +} + +func ChangeZitadelIDPInstanceRolesInfo(instanceRolesInfo []RolesInfo) ZitadelIDPChanges { + return func(e *ZitadelIDPChangedEvent) { + // explicitly set them to empty in case the instance roles are unset + if instanceRolesInfo == nil { + instanceRolesInfo = make([]RolesInfo, 0) + } + e.InstanceRolesInfo = &instanceRolesInfo + } +} + +func ChangeZitadelIDPOptions(options OptionChanges) ZitadelIDPChanges { + return func(e *ZitadelIDPChangedEvent) { + e.OptionChanges = options + } +} + +func (e *ZitadelIDPChangedEvent) Payload() interface{} { + return e +} + +func (e *ZitadelIDPChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil } diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index f9d7dd65b..37f1bbb66 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -106,6 +106,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SAMLIDPChangedEventType, SAMLIDPChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, ZitadelIDPAddedEventType, eventstore.GenericEventMapper[ZitadelIDPAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, ZitadelIDPChangedEventType, eventstore.GenericEventMapper[ZitadelIDPChangedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderCascadeRemovedEventType, IdentityProviderCascadeRemovedEventMapper) diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index 797adc368..eb6883890 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -39,6 +39,7 @@ const ( SAMLIDPChangedEventType eventstore.EventType = "instance.idp.saml.changed" IDPRemovedEventType eventstore.EventType = "instance.idp.removed" ZitadelIDPAddedEventType eventstore.EventType = "instance.idp.zitadel.added" + ZitadelIDPChangedEventType eventstore.EventType = "instance.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 +}