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>
This commit is contained in:
Gayathri VijayanandCopilot Autofix powered by AI authored and GitHub committed 2026-07-06 10:18:35 +02:00
1 parent 1beabed8cf
commit 653a968d91
13 files changed
+1327 -16

No files matched your search

+10
View File
@@ -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
}
+27 -9
View File
@@ -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
}
+96 -1
View File
@@ -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)
})
}
}
@@ -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
})
}
}
+61 -2
View File
@@ -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
}
+70 -3
View File
@@ -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")
}
+28
View File
@@ -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
}
+504
View File
@@ -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)
}
})
}
}
+63
View File
@@ -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
}
@@ -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) {
+84 -1
View File
@@ -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
}
@@ -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)
+27
View File
@@ -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
}