diff --git a/console/angular.json b/console/angular.json index fa3a3712d..af04bd3b5 100644 --- a/console/angular.json +++ b/console/angular.json @@ -86,7 +86,7 @@ { "type": "initial", "maximumWarning": "6mb", - "maximumError": "6mb" + "maximumError": "7mb" }, { "type": "anyComponentStyle", diff --git a/docs/docs/apis/assets/assets.md b/docs/docs/apis/assets/assets.md old mode 100644 new mode 100755 index 4a9b5cfb9..28bec4fa1 --- a/docs/docs/apis/assets/assets.md +++ b/docs/docs/apis/assets/assets.md @@ -256,4 +256,4 @@ GET: /users/me/avatar - \ No newline at end of file + diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index 5d3ca5700..921378e05 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -174,8 +174,29 @@ func (s *Server) ListProviders(ctx context.Context, req *admin_pb.ListProvidersR }, nil } +func (s *Server) AddGenericOAuthProvider(ctx context.Context, req *admin_pb.AddGenericOAuthProviderRequest) (*admin_pb.AddGenericOAuthProviderResponse, error) { + id, details, err := s.command.AddInstanceGenericOAuthProvider(ctx, addGenericOAuthProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.AddGenericOAuthProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) UpdateGenericOAuthProvider(ctx context.Context, req *admin_pb.UpdateGenericOAuthProviderRequest) (*admin_pb.UpdateGenericOAuthProviderResponse, error) { + details, err := s.command.UpdateInstanceGenericOAuthProvider(ctx, req.Id, updateGenericOAuthProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateGenericOAuthProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) AddGoogleProvider(ctx context.Context, req *admin_pb.AddGoogleProviderRequest) (*admin_pb.AddGoogleProviderResponse, error) { - id, details, err := s.command.AddOrgGoogleProvider(ctx, authz.GetCtxData(ctx).OrgID, addGoogleProviderToCommand(req)) + id, details, err := s.command.AddInstanceGoogleProvider(ctx, addGoogleProviderToCommand(req)) if err != nil { return nil, err } @@ -186,7 +207,7 @@ func (s *Server) AddGoogleProvider(ctx context.Context, req *admin_pb.AddGoogleP } func (s *Server) UpdateGoogleProvider(ctx context.Context, req *admin_pb.UpdateGoogleProviderRequest) (*admin_pb.UpdateGoogleProviderResponse, error) { - details, err := s.command.UpdateOrgGoogleProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateGoogleProviderToCommand(req)) + details, err := s.command.UpdateInstanceGoogleProvider(ctx, req.Id, updateGoogleProviderToCommand(req)) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 1bf605088..e08065e19 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -201,6 +201,32 @@ func providerQueryToQuery(idpQuery *admin_pb.ProviderQuery) (query.SearchQuery, } } +func addGenericOAuthProviderToCommand(req *admin_pb.AddGenericOAuthProviderRequest) command.GenericOAuthProvider { + return command.GenericOAuthProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + AuthorizationEndpoint: req.AuthorizationEndpoint, + TokenEndpoint: req.TokenEndpoint, + UserEndpoint: req.UserEndpoint, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateGenericOAuthProviderToCommand(req *admin_pb.UpdateGenericOAuthProviderRequest) command.GenericOAuthProvider { + return command.GenericOAuthProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + AuthorizationEndpoint: req.AuthorizationEndpoint, + TokenEndpoint: req.TokenEndpoint, + UserEndpoint: req.UserEndpoint, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + func addGoogleProviderToCommand(req *admin_pb.AddGoogleProviderRequest) command.GoogleProvider { return command.GoogleProvider{ Name: req.Name, diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index 9724703bf..0526b50f7 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -402,31 +402,57 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig { IsAutoUpdate: config.IsAutoUpdate, }, } + if config.OAuthIDPTemplate != nil { + oauthConfigToPb(providerConfig, config.OAuthIDPTemplate) + return providerConfig + } if config.GoogleIDPTemplate != nil { - providerConfig.Config = &idp_pb.ProviderConfig_Google{ - Google: &idp_pb.GoogleConfig{ - ClientId: config.GoogleIDPTemplate.ClientID, - Scopes: config.GoogleIDPTemplate.Scopes, - }, - } + googleConfigToPb(providerConfig, config.GoogleIDPTemplate) + return providerConfig } if config.LDAPIDPTemplate != nil { - providerConfig.Config = &idp_pb.ProviderConfig_Ldap{ - Ldap: &idp_pb.LDAPConfig{ - Host: config.Host, - Port: config.Port, - Tls: config.TLS, - BaseDn: config.BaseDN, - UserObjectClass: config.UserObjectClass, - UserUniqueAttribute: config.UserUniqueAttribute, - Admin: config.Admin, - Attributes: ldapAttributesToPb(config.LDAPAttributes), - }, - } + ldapConfigToPb(providerConfig, config.LDAPIDPTemplate) + return providerConfig } return providerConfig } +func googleConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.GoogleIDPTemplate) { + providerConfig.Config = &idp_pb.ProviderConfig_Google{ + Google: &idp_pb.GoogleConfig{ + ClientId: template.ClientID, + Scopes: template.Scopes, + }, + } +} + +func oauthConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.OAuthIDPTemplate) { + providerConfig.Config = &idp_pb.ProviderConfig_Oauth{ + Oauth: &idp_pb.OAuthConfig{ + ClientId: template.ClientID, + AuthorizationEndpoint: template.AuthorizationEndpoint, + TokenEndpoint: template.TokenEndpoint, + UserEndpoint: template.UserEndpoint, + Scopes: template.Scopes, + }, + } +} + +func ldapConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.LDAPIDPTemplate) { + providerConfig.Config = &idp_pb.ProviderConfig_Ldap{ + Ldap: &idp_pb.LDAPConfig{ + Host: template.Host, + Port: template.Port, + Tls: template.TLS, + BaseDn: template.BaseDN, + UserObjectClass: template.UserObjectClass, + UserUniqueAttribute: template.UserUniqueAttribute, + Admin: template.Admin, + Attributes: ldapAttributesToPb(template.LDAPAttributes), + }, + } +} + func ldapAttributesToPb(attributes idp.LDAPAttributes) *idp_pb.LDAPAttributes { return &idp_pb.LDAPAttributes{ IdAttribute: attributes.IDAttribute, diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 0af131d54..5dc88be53 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -166,6 +166,27 @@ func (s *Server) ListProviders(ctx context.Context, req *mgmt_pb.ListProvidersRe }, nil } +func (s *Server) AddGenericOAuthProvider(ctx context.Context, req *mgmt_pb.AddGenericOAuthProviderRequest) (*mgmt_pb.AddGenericOAuthProviderResponse, error) { + id, details, err := s.command.AddOrgGenericOAuthProvider(ctx, authz.GetCtxData(ctx).OrgID, addGenericOAuthProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.AddGenericOAuthProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) UpdateGenericOAuthProvider(ctx context.Context, req *mgmt_pb.UpdateGenericOAuthProviderRequest) (*mgmt_pb.UpdateGenericOAuthProviderResponse, error) { + details, err := s.command.UpdateOrgGenericOAuthProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateGenericOAuthProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateGenericOAuthProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) AddGoogleProvider(ctx context.Context, req *mgmt_pb.AddGoogleProviderRequest) (*mgmt_pb.AddGoogleProviderResponse, error) { id, details, err := s.command.AddOrgGoogleProvider(ctx, authz.GetCtxData(ctx).OrgID, addGoogleProviderToCommand(req)) if err != nil { diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index 792f87684..ed863591a 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -218,6 +218,32 @@ func providerQueryToQuery(idpQuery *mgmt_pb.ProviderQuery) (query.SearchQuery, e } } +func addGenericOAuthProviderToCommand(req *mgmt_pb.AddGenericOAuthProviderRequest) command.GenericOAuthProvider { + return command.GenericOAuthProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + AuthorizationEndpoint: req.AuthorizationEndpoint, + TokenEndpoint: req.TokenEndpoint, + UserEndpoint: req.UserEndpoint, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateGenericOAuthProviderToCommand(req *mgmt_pb.UpdateGenericOAuthProviderRequest) command.GenericOAuthProvider { + return command.GenericOAuthProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + AuthorizationEndpoint: req.AuthorizationEndpoint, + TokenEndpoint: req.TokenEndpoint, + UserEndpoint: req.UserEndpoint, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + func addGoogleProviderToCommand(req *mgmt_pb.AddGoogleProviderRequest) command.GoogleProvider { return command.GoogleProvider{ Name: req.Name, diff --git a/internal/command/idp.go b/internal/command/idp.go index 60f33ff5e..2d2295c29 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -2,6 +2,17 @@ package command import "github.com/zitadel/zitadel/internal/repository/idp" +type GenericOAuthProvider struct { + Name string + ClientID string + ClientSecret string + AuthorizationEndpoint string + TokenEndpoint string + UserEndpoint string + Scopes []string + IDPOptions idp.Options +} + type GoogleProvider struct { Name string ClientID string diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 1431f1412..424e8a671 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -10,6 +10,118 @@ import ( "github.com/zitadel/zitadel/internal/repository/idpconfig" ) +type OAuthIDPWriteModel struct { + eventstore.WriteModel + + Name string + ID string + ClientID string + ClientSecret *crypto.CryptoValue + AuthorizationEndpoint string + TokenEndpoint string + UserEndpoint string + Scopes []string + idp.Options + + State domain.IDPState +} + +func (wm *OAuthIDPWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *idp.OAuthIDPAddedEvent: + wm.reduceAddedEvent(e) + case *idp.OAuthIDPChangedEvent: + wm.reduceChangedEvent(e) + case *idp.RemovedEvent: + wm.State = domain.IDPStateRemoved + } + } + return wm.WriteModel.Reduce() +} + +func (wm *OAuthIDPWriteModel) reduceAddedEvent(e *idp.OAuthIDPAddedEvent) { + wm.Name = e.Name + wm.ClientID = e.ClientID + wm.ClientSecret = e.ClientSecret + wm.AuthorizationEndpoint = e.AuthorizationEndpoint + wm.TokenEndpoint = e.TokenEndpoint + wm.UserEndpoint = e.UserEndpoint + wm.Scopes = e.Scopes + wm.State = domain.IDPStateActive +} + +func (wm *OAuthIDPWriteModel) reduceChangedEvent(e *idp.OAuthIDPChangedEvent) { + 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.AuthorizationEndpoint != nil { + wm.AuthorizationEndpoint = *e.AuthorizationEndpoint + } + if e.TokenEndpoint != nil { + wm.TokenEndpoint = *e.TokenEndpoint + } + if e.UserEndpoint != nil { + wm.UserEndpoint = *e.UserEndpoint + } + if e.Scopes != nil { + wm.Scopes = e.Scopes + } + wm.Options.ReduceChanges(e.OptionChanges) +} + +func (wm *OAuthIDPWriteModel) NewChanges( + name, + clientID, + clientSecretString string, + secretCrypto crypto.Crypto, + authorizationEndpoint, + tokenEndpoint, + userEndpoint string, + scopes []string, + options idp.Options, +) ([]idp.OAuthIDPChanges, error) { + changes := make([]idp.OAuthIDPChanges, 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.ChangeOAuthClientSecret(clientSecret)) + } + if wm.ClientID != clientID { + changes = append(changes, idp.ChangeOAuthClientID(clientID)) + } + if wm.Name != name { + changes = append(changes, idp.ChangeOAuthName(name)) + } + if wm.AuthorizationEndpoint != authorizationEndpoint { + changes = append(changes, idp.ChangeOAuthAuthorizationEndpoint(authorizationEndpoint)) + } + if wm.TokenEndpoint != tokenEndpoint { + changes = append(changes, idp.ChangeOAuthTokenEndpoint(tokenEndpoint)) + } + if wm.UserEndpoint != userEndpoint { + changes = append(changes, idp.ChangeOAuthUserEndpoint(userEndpoint)) + } + if !reflect.DeepEqual(wm.Scopes, scopes) { + changes = append(changes, idp.ChangeOAuthScopes(scopes)) + } + opts := wm.Options.Changes(options) + if !opts.IsZero() { + changes = append(changes, idp.ChangeOAuthOptions(opts)) + } + return changes, nil +} + type GoogleIDPWriteModel struct { eventstore.WriteModel @@ -259,6 +371,10 @@ type IDPRemoveWriteModel struct { func (wm *IDPRemoveWriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { + case *idp.OAuthIDPAddedEvent: + wm.reduceAdded(e.ID, e.Name) + case *idp.OAuthIDPChangedEvent: + wm.reduceChanged(e.ID, e.Name) case *idp.GoogleIDPAddedEvent: wm.reduceAdded(e.ID, e.Name) case *idp.GoogleIDPChangedEvent: diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index e970c5033..0f37218cf 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -13,13 +13,57 @@ import ( "github.com/zitadel/zitadel/internal/repository/instance" ) -func (c *Commands) AddInstanceGoogleProvider(ctx context.Context, provider GoogleProvider) (string, *domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) +func (c *Commands) AddInstanceGenericOAuthProvider(ctx context.Context, provider GenericOAuthProvider) (string, *domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) id, err := c.idGenerator.Next() if err != nil { return "", nil, err } - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceGoogleProvider(instanceAgg, id, provider)) + writeModel := NewOAuthInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceOAuthProvider(instanceAgg, writeModel, provider)) + if err != nil { + return "", nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return "", nil, err + } + return id, pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) UpdateInstanceGenericOAuthProvider(ctx context.Context, id string, provider GenericOAuthProvider) (*domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + writeModel := NewOAuthInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceOAuthProvider(instanceAgg, writeModel, provider)) + 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) AddInstanceGoogleProvider(ctx context.Context, provider GoogleProvider) (string, *domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + writeModel := NewGoogleInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceGoogleProvider(instanceAgg, writeModel, provider)) if err != nil { return "", nil, err } @@ -31,14 +75,20 @@ func (c *Commands) AddInstanceGoogleProvider(ctx context.Context, provider Googl } func (c *Commands) UpdateInstanceGoogleProvider(ctx context.Context, id string, provider GoogleProvider) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceGoogleProvider(instanceAgg, id, provider)) + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + writeModel := NewGoogleInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceGoogleProvider(instanceAgg, writeModel, provider)) if err != nil { return nil, err } if len(cmds) == 0 { // no change, so return directly - return &domain.ObjectDetails{}, nil + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { @@ -48,12 +98,14 @@ func (c *Commands) UpdateInstanceGoogleProvider(ctx context.Context, id string, } func (c *Commands) AddInstanceLDAPProvider(ctx context.Context, provider LDAPProvider) (string, *domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) id, err := c.idGenerator.Next() if err != nil { return "", nil, err } - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceLDAPProvider(instanceAgg, id, provider)) + writeModel := NewLDAPInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceLDAPProvider(instanceAgg, writeModel, provider)) if err != nil { return "", nil, err } @@ -65,14 +117,20 @@ func (c *Commands) AddInstanceLDAPProvider(ctx context.Context, provider LDAPPro } func (c *Commands) UpdateInstanceLDAPProvider(ctx context.Context, id string, provider LDAPProvider) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceLDAPProvider(instanceAgg, id, provider)) + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + writeModel := NewLDAPInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceLDAPProvider(instanceAgg, writeModel, provider)) if err != nil { return nil, err } if len(cmds) == 0 { // no change, so return directly - return &domain.ObjectDetails{}, nil + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { @@ -94,7 +152,113 @@ func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*doma return pushedEventsToObjectDetails(pushedEvents), nil } -func (c *Commands) prepareAddInstanceGoogleProvider(a *instance.Aggregate, id string, provider GoogleProvider) preparation.Validation { +func (c *Commands) prepareAddInstanceOAuthProvider(a *instance.Aggregate, writeModel *InstanceOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-D32ef", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dbgzf", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-DF4ga", "Errors.Invalid.Argument") + } + if provider.AuthorizationEndpoint = strings.TrimSpace(provider.AuthorizationEndpoint); provider.AuthorizationEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-B23bs", "Errors.Invalid.Argument") + } + if provider.TokenEndpoint = strings.TrimSpace(provider.TokenEndpoint); provider.TokenEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-D2gj8", "Errors.Invalid.Argument") + } + if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fb8jk", "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 + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + instance.NewOAuthIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.AuthorizationEndpoint, + provider.TokenEndpoint, + provider.UserEndpoint, + provider.Scopes, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateInstanceOAuthProvider(a *instance.Aggregate, writeModel *InstanceOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-D32ef", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dbgzf", "Errors.Invalid.Argument") + } + if provider.AuthorizationEndpoint = strings.TrimSpace(provider.AuthorizationEndpoint); provider.AuthorizationEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-B23bs", "Errors.Invalid.Argument") + } + if provider.TokenEndpoint = strings.TrimSpace(provider.TokenEndpoint); provider.TokenEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-D2gj8", "Errors.Invalid.Argument") + } + if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fb8jk", "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, caos_errs.ThrowNotFound(nil, "INST-D3r1s", "Errors.Instance.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + provider.ClientSecret, + c.idpConfigEncryption, + provider.AuthorizationEndpoint, + provider.TokenEndpoint, + provider.UserEndpoint, + provider.Scopes, + provider.IDPOptions, + ) + if err != nil { + return nil, err + } + if event == nil { + return nil, nil + } + return []eventstore.Command{event}, nil + }, nil + } +} + +func (c *Commands) prepareAddInstanceGoogleProvider(a *instance.Aggregate, writeModel *InstanceGoogleIDPWriteModel, provider GoogleProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-D3fvs", "Errors.Invalid.Argument") @@ -103,7 +267,6 @@ func (c *Commands) prepareAddInstanceGoogleProvider(a *instance.Aggregate, id st return nil, caos_errs.ThrowInvalidArgument(nil, "INST-W2vqs", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewGoogleInstanceIDPWriteModel(a.InstanceID, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -120,7 +283,7 @@ func (c *Commands) prepareAddInstanceGoogleProvider(a *instance.Aggregate, id st instance.NewGoogleIDPAddedEvent( ctx, &a.Aggregate, - id, + writeModel.ID, provider.Name, provider.ClientID, secret, @@ -132,16 +295,15 @@ func (c *Commands) prepareAddInstanceGoogleProvider(a *instance.Aggregate, id st } } -func (c *Commands) prepareUpdateInstanceGoogleProvider(a *instance.Aggregate, id string, provider GoogleProvider) preparation.Validation { +func (c *Commands) prepareUpdateInstanceGoogleProvider(a *instance.Aggregate, writeModel *InstanceGoogleIDPWriteModel, provider GoogleProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { - if id = strings.TrimSpace(id); id == "" { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-S32t1", "Errors.Invalid.Argument") } if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-ds432", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewGoogleInstanceIDPWriteModel(a.InstanceID, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -156,7 +318,7 @@ func (c *Commands) prepareUpdateInstanceGoogleProvider(a *instance.Aggregate, id event, err := writeModel.NewChangedEvent( ctx, &a.Aggregate, - id, + writeModel.ID, provider.Name, provider.ClientID, provider.ClientSecret, @@ -175,7 +337,7 @@ func (c *Commands) prepareUpdateInstanceGoogleProvider(a *instance.Aggregate, id } } -func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, id string, provider LDAPProvider) preparation.Validation { +func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, writeModel *InstanceLDAPIDPWriteModel, provider LDAPProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAfdd", "Errors.Invalid.Argument") @@ -199,7 +361,6 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, id stri return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdf5h", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewLDAPInstanceIDPWriteModel(a.InstanceID, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -216,7 +377,7 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, id stri instance.NewLDAPIDPAddedEvent( ctx, &a.Aggregate, - id, + writeModel.ID, provider.Name, provider.Host, provider.Port, @@ -234,9 +395,9 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, id stri } } -func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, id string, provider LDAPProvider) preparation.Validation { +func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writeModel *InstanceLDAPIDPWriteModel, provider LDAPProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { - if id = strings.TrimSpace(id); id == "" { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dgdbs", "Errors.Invalid.Argument") } if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { @@ -258,7 +419,6 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, id s return nil, caos_errs.ThrowInvalidArgument(nil, "INST-DG45z", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewLDAPInstanceIDPWriteModel(a.InstanceID, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -273,7 +433,7 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, id s event, err := writeModel.NewChangedEvent( ctx, &a.Aggregate, - id, + writeModel.ID, writeModel.Name, provider.Name, provider.Host, diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index d9757c323..42fa70ef4 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -9,6 +9,89 @@ import ( "github.com/zitadel/zitadel/internal/repository/instance" ) +type InstanceOAuthIDPWriteModel struct { + OAuthIDPWriteModel +} + +func NewOAuthInstanceIDPWriteModel(instanceID, id string) *InstanceOAuthIDPWriteModel { + return &InstanceOAuthIDPWriteModel{ + OAuthIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + }, + ID: id, + }, + } +} + +func (wm *InstanceOAuthIDPWriteModel) Reduce() error { + return wm.OAuthIDPWriteModel.Reduce() +} + +func (wm *InstanceOAuthIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *instance.OAuthIDPAddedEvent: + wm.OAuthIDPWriteModel.AppendEvents(&e.OAuthIDPAddedEvent) + case *instance.OAuthIDPChangedEvent: + wm.OAuthIDPWriteModel.AppendEvents(&e.OAuthIDPChangedEvent) + case *instance.IDPRemovedEvent: + wm.OAuthIDPWriteModel.AppendEvents(&e.RemovedEvent) + } + } +} + +func (wm *InstanceOAuthIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.OAuthIDPAddedEventType, + instance.OAuthIDPChangedEventType, + instance.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *InstanceOAuthIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID, + clientSecretString string, + secretCrypto crypto.Crypto, + authorizationEndpoint, + tokenEndpoint, + userEndpoint string, + scopes []string, + options idp.Options, +) (*instance.OAuthIDPChangedEvent, error) { + + changes, err := wm.OAuthIDPWriteModel.NewChanges( + name, + clientID, + clientSecretString, + secretCrypto, + authorizationEndpoint, + tokenEndpoint, + userEndpoint, + scopes, + options, + ) + if err != nil { + return nil, err + } + if len(changes) == 0 { + return nil, nil + } + return instance.NewOAuthIDPChangedEvent(ctx, aggregate, id, changes) +} + type InstanceGoogleIDPWriteModel struct { GoogleIDPWriteModel } @@ -76,11 +159,7 @@ func (wm *InstanceGoogleIDPWriteModel) NewChangedEvent( if len(changes) == 0 { return nil, nil } - changeEvent, err := instance.NewGoogleIDPChangedEvent(ctx, aggregate, id, changes) - if err != nil { - return nil, err - } - return changeEvent, nil + return instance.NewGoogleIDPChangedEvent(ctx, aggregate, id, changes) } type InstanceLDAPIDPWriteModel struct { @@ -107,19 +186,10 @@ func (wm *InstanceLDAPIDPWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *instance.LDAPIDPAddedEvent: - if wm.ID != e.ID { - continue - } wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) case *instance.LDAPIDPChangedEvent: - if wm.ID != e.ID { - continue - } wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPChangedEvent) case *instance.IDPRemovedEvent: - if wm.ID != e.ID { - continue - } wm.LDAPIDPWriteModel.AppendEvents(&e.RemovedEvent) default: wm.LDAPIDPWriteModel.AppendEvents(e) @@ -138,6 +208,7 @@ func (wm *InstanceLDAPIDPWriteModel) Query() *eventstore.SearchQueryBuilder { instance.LDAPIDPChangedEventType, instance.IDPRemovedEventType, ). + EventData(map[string]interface{}{"id": wm.ID}). Builder() } @@ -180,11 +251,7 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent( if len(changes) == 0 { return nil, nil } - changeEvent, err := instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes) - if err != nil { - return nil, err - } - return changeEvent, nil + return instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes) } type InstanceIDPRemoveWriteModel struct { @@ -210,6 +277,10 @@ func (wm *InstanceIDPRemoveWriteModel) Reduce() error { func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { + case *instance.OAuthIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.OAuthIDPAddedEvent) + case *instance.OAuthIDPChangedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.OAuthIDPChangedEvent) case *instance.GoogleIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) case *instance.GoogleIDPChangedEvent: @@ -233,11 +304,14 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { AggregateTypes(instance.AggregateType). AggregateIDs(wm.AggregateID). EventTypes( + instance.OAuthIDPAddedEventType, + instance.OAuthIDPChangedEventType, instance.GoogleIDPAddedEventType, instance.GoogleIDPChangedEventType, instance.LDAPIDPAddedEventType, instance.LDAPIDPChangedEventType, instance.IDPRemovedEventType, ). + EventData(map[string]interface{}{"id": wm.ID}). Builder() } diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index d1279e113..ffeddf96b 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -20,6 +20,542 @@ import ( "github.com/zitadel/zitadel/internal/repository/instance" ) +func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + provider GenericOAuthProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{ + Name: "name", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientSecret", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid auth endpoint", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid token endpoint", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid user endpoint", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + nil, + idp.Options{}, + )), + }, + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "ok all set", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + []string{"user"}, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + )), + }, + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + Scopes: []string{"user"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + idpConfigEncryption: tt.fields.secretCrypto, + } + id, got, err := c.AddInstanceGenericOAuthProvider(tt.args.ctx, 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, id) + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + id string + provider GenericOAuthProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GenericOAuthProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid auth endpoint", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid token endpoint", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid user endpoint", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + nil, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + nil, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + func() eventstore.Command { + t := true + event, _ := instance.NewOAuthIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + []idp.OAuthIDPChanges{ + idp.ChangeOAuthName("new name"), + idp.ChangeOAuthClientID("clientID2"), + idp.ChangeOAuthClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + idp.ChangeOAuthAuthorizationEndpoint("new auth"), + idp.ChangeOAuthTokenEndpoint("new token"), + idp.ChangeOAuthUserEndpoint("new user"), + idp.ChangeOAuthScopes([]string{"openid", "profile"}), + idp.ChangeOAuthOptions(idp.OptionChanges{ + IsCreationAllowed: &t, + IsLinkingAllowed: &t, + IsAutoCreation: &t, + IsAutoUpdate: &t, + }), + }, + ) + return event + }(), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GenericOAuthProvider{ + Name: "new name", + ClientID: "clientID2", + ClientSecret: "newSecret", + AuthorizationEndpoint: "new auth", + TokenEndpoint: "new token", + UserEndpoint: "new user", + Scopes: []string{"openid", "profile"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + 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, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.UpdateInstanceGenericOAuthProvider(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 { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + func TestCommandSide_AddInstanceGoogleIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -281,7 +817,7 @@ func TestCommandSide_UpdateInstanceGoogleIDP(t *testing.T) { }, }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, }, }, { @@ -893,7 +1429,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { }, }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, }, }, { diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index 97b95b33c..6cae2f480 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -12,13 +12,54 @@ import ( "github.com/zitadel/zitadel/internal/repository/org" ) +func (c *Commands) AddOrgGenericOAuthProvider(ctx context.Context, resourceOwner string, provider GenericOAuthProvider) (string, *domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + writeModel := NewOAuthOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgOAuthProvider(orgAgg, writeModel, provider)) + if err != nil { + return "", nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return "", nil, err + } + return id, pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) UpdateOrgGenericOAuthProvider(ctx context.Context, resourceOwner, id string, provider GenericOAuthProvider) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + writeModel := NewOAuthOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgOAuthProvider(orgAgg, writeModel, provider)) + 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) AddOrgGoogleProvider(ctx context.Context, resourceOwner string, provider GoogleProvider) (string, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) id, err := c.idGenerator.Next() if err != nil { return "", nil, err } - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgGoogleProvider(orgAgg, resourceOwner, id, provider)) + writeModel := NewGoogleOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgGoogleProvider(orgAgg, writeModel, provider)) if err != nil { return "", nil, err } @@ -31,13 +72,18 @@ func (c *Commands) AddOrgGoogleProvider(ctx context.Context, resourceOwner strin func (c *Commands) UpdateOrgGoogleProvider(ctx context.Context, resourceOwner, id string, provider GoogleProvider) (*domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgGoogleProvider(orgAgg, resourceOwner, id, provider)) + writeModel := NewGoogleOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgGoogleProvider(orgAgg, writeModel, provider)) if err != nil { return nil, err } if len(cmds) == 0 { // no change, so return directly - return &domain.ObjectDetails{}, nil + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { @@ -52,7 +98,8 @@ func (c *Commands) AddOrgLDAPProvider(ctx context.Context, resourceOwner string, if err != nil { return "", nil, err } - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgLDAPProvider(orgAgg, resourceOwner, id, provider)) + writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgLDAPProvider(orgAgg, writeModel, provider)) if err != nil { return "", nil, err } @@ -65,13 +112,18 @@ func (c *Commands) AddOrgLDAPProvider(ctx context.Context, resourceOwner string, func (c *Commands) UpdateOrgLDAPProvider(ctx context.Context, resourceOwner, id string, provider LDAPProvider) (*domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgLDAPProvider(orgAgg, resourceOwner, id, provider)) + writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgLDAPProvider(orgAgg, writeModel, provider)) if err != nil { return nil, err } if len(cmds) == 0 { // no change, so return directly - return &domain.ObjectDetails{}, nil + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { @@ -93,16 +145,27 @@ func (c *Commands) DeleteOrgProvider(ctx context.Context, resourceOwner, id stri return pushedEventsToObjectDetails(pushedEvents), nil } -func (c *Commands) prepareAddOrgGoogleProvider(a *org.Aggregate, resourceOwner, id string, provider GoogleProvider) preparation.Validation { +func (c *Commands) prepareAddOrgOAuthProvider(a *org.Aggregate, writeModel *OrgOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-D32ef", "Errors.Invalid.Argument") + } if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-D3fvs", "Errors.Invalid.Argument") + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dbgzf", "Errors.Invalid.Argument") } if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-W2vqs", "Errors.Invalid.Argument") + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-DF4ga", "Errors.Invalid.Argument") + } + if provider.AuthorizationEndpoint = strings.TrimSpace(provider.AuthorizationEndpoint); provider.AuthorizationEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-B23bs", "Errors.Invalid.Argument") + } + if provider.TokenEndpoint = strings.TrimSpace(provider.TokenEndpoint); provider.TokenEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-D2gj8", "Errors.Invalid.Argument") + } + if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fb8jk", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewGoogleOrgIDPWriteModel(resourceOwner, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -116,22 +179,127 @@ func (c *Commands) prepareAddOrgGoogleProvider(a *org.Aggregate, resourceOwner, return nil, err } return []eventstore.Command{ - org.NewGoogleIDPAddedEvent(ctx, &a.Aggregate, id, provider.Name, provider.ClientID, secret, provider.Scopes, provider.IDPOptions), + org.NewOAuthIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.AuthorizationEndpoint, + provider.TokenEndpoint, + provider.UserEndpoint, + provider.Scopes, + provider.IDPOptions, + ), }, nil }, nil } } -func (c *Commands) prepareUpdateOrgGoogleProvider(a *org.Aggregate, resourceOwner, id string, provider GoogleProvider) preparation.Validation { +func (c *Commands) prepareUpdateOrgOAuthProvider(a *org.Aggregate, writeModel *OrgOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { - if id = strings.TrimSpace(id); id == "" { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-asfsa", "Errors.Invalid.Argument") + } + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-D32ef", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dbgzf", "Errors.Invalid.Argument") + } + if provider.AuthorizationEndpoint = strings.TrimSpace(provider.AuthorizationEndpoint); provider.AuthorizationEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-B23bs", "Errors.Invalid.Argument") + } + if provider.TokenEndpoint = strings.TrimSpace(provider.TokenEndpoint); provider.TokenEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-D2gj8", "Errors.Invalid.Argument") + } + if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fb8jk", "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, caos_errs.ThrowNotFound(nil, "ORG-JNsd3", "Errors.Org.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + provider.ClientSecret, + c.idpConfigEncryption, + provider.AuthorizationEndpoint, + provider.TokenEndpoint, + provider.UserEndpoint, + provider.Scopes, + provider.IDPOptions, + ) + if err != nil { + return nil, err + } + if event == nil { + return nil, nil + } + return []eventstore.Command{event}, nil + }, nil + } +} + +func (c *Commands) prepareAddOrgGoogleProvider(a *org.Aggregate, writeModel *OrgGoogleIDPWriteModel, provider GoogleProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-D3fvs", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-W2vqs", "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 + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + org.NewGoogleIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateOrgGoogleProvider(a *org.Aggregate, writeModel *OrgGoogleIDPWriteModel, provider GoogleProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-S32t1", "Errors.Invalid.Argument") } if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-ds432", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewGoogleOrgIDPWriteModel(resourceOwner, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -146,7 +314,7 @@ func (c *Commands) prepareUpdateOrgGoogleProvider(a *org.Aggregate, resourceOwne event, err := writeModel.NewChangedEvent( ctx, &a.Aggregate, - id, + writeModel.ID, provider.Name, provider.ClientID, provider.ClientSecret, @@ -165,7 +333,7 @@ func (c *Commands) prepareUpdateOrgGoogleProvider(a *org.Aggregate, resourceOwne } } -func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, resourceOwner, id string, provider LDAPProvider) preparation.Validation { +func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, writeModel *OrgLDAPIDPWriteModel, provider LDAPProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAfdd", "Errors.Invalid.Argument") @@ -189,7 +357,6 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, resourceOwner, id return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdf5h", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -206,7 +373,7 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, resourceOwner, id org.NewLDAPIDPAddedEvent( ctx, &a.Aggregate, - id, + writeModel.ID, provider.Name, provider.Host, provider.Port, @@ -224,9 +391,9 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, resourceOwner, id } } -func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, resourceOwner, id string, provider LDAPProvider) preparation.Validation { +func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, writeModel *OrgLDAPIDPWriteModel, provider LDAPProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { - if id = strings.TrimSpace(id); id == "" { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dgdbs", "Errors.Invalid.Argument") } if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { @@ -248,7 +415,6 @@ func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, resourceOwner, return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-DG45z", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err @@ -263,7 +429,7 @@ func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, resourceOwner, event, err := writeModel.NewChangedEvent( ctx, &a.Aggregate, - id, + writeModel.ID, writeModel.Name, provider.Name, provider.Host, diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index e9b0d5077..8192f18fd 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -9,6 +9,91 @@ import ( "github.com/zitadel/zitadel/internal/repository/org" ) +type OrgOAuthIDPWriteModel struct { + OAuthIDPWriteModel +} + +func NewOAuthOrgIDPWriteModel(orgID, id string) *OrgOAuthIDPWriteModel { + return &OrgOAuthIDPWriteModel{ + OAuthIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + ID: id, + }, + } +} + +func (wm *OrgOAuthIDPWriteModel) Reduce() error { + return wm.OAuthIDPWriteModel.Reduce() +} + +func (wm *OrgOAuthIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *org.OAuthIDPAddedEvent: + wm.OAuthIDPWriteModel.AppendEvents(&e.OAuthIDPAddedEvent) + case *org.OAuthIDPChangedEvent: + wm.OAuthIDPWriteModel.AppendEvents(&e.OAuthIDPChangedEvent) + case *org.IDPRemovedEvent: + wm.OAuthIDPWriteModel.AppendEvents(&e.RemovedEvent) + default: + wm.OAuthIDPWriteModel.AppendEvents(e) + } + } +} + +func (wm *OrgOAuthIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + org.OAuthIDPAddedEventType, + org.OAuthIDPChangedEventType, + org.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *OrgOAuthIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID, + clientSecretString string, + secretCrypto crypto.Crypto, + authorizationEndpoint, + tokenEndpoint, + userEndpoint string, + scopes []string, + options idp.Options, +) (*org.OAuthIDPChangedEvent, error) { + + changes, err := wm.OAuthIDPWriteModel.NewChanges( + name, + clientID, + clientSecretString, + secretCrypto, + authorizationEndpoint, + tokenEndpoint, + userEndpoint, + scopes, + options, + ) + if err != nil { + return nil, err + } + if len(changes) == 0 { + return nil, nil + } + return org.NewOAuthIDPChangedEvent(ctx, aggregate, id, changes) +} + type OrgGoogleIDPWriteModel struct { GoogleIDPWriteModel } @@ -78,11 +163,7 @@ func (wm *OrgGoogleIDPWriteModel) NewChangedEvent( if len(changes) == 0 { return nil, nil } - changeEvent, err := org.NewGoogleIDPChangedEvent(ctx, aggregate, id, changes) - if err != nil { - return nil, err - } - return changeEvent, nil + return org.NewGoogleIDPChangedEvent(ctx, aggregate, id, changes) } type OrgLDAPIDPWriteModel struct { @@ -109,19 +190,10 @@ func (wm *OrgLDAPIDPWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *org.LDAPIDPAddedEvent: - if wm.ID != e.ID { - continue - } wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) case *org.LDAPIDPChangedEvent: - if wm.ID != e.ID { - continue - } wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPChangedEvent) case *org.IDPRemovedEvent: - if wm.ID != e.ID { - continue - } wm.LDAPIDPWriteModel.AppendEvents(&e.RemovedEvent) default: wm.LDAPIDPWriteModel.AppendEvents(e) @@ -140,6 +212,7 @@ func (wm *OrgLDAPIDPWriteModel) Query() *eventstore.SearchQueryBuilder { org.LDAPIDPChangedEventType, org.IDPRemovedEventType, ). + EventData(map[string]interface{}{"id": wm.ID}). Builder() } @@ -182,11 +255,7 @@ func (wm *OrgLDAPIDPWriteModel) NewChangedEvent( if len(changes) == 0 { return nil, nil } - changeEvent, err := org.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes) - if err != nil { - return nil, err - } - return changeEvent, nil + return org.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes) } type OrgIDPRemoveWriteModel struct { @@ -212,6 +281,10 @@ func (wm *OrgIDPRemoveWriteModel) Reduce() error { func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { + case *org.OAuthIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.OAuthIDPAddedEvent) + case *org.OAuthIDPChangedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.OAuthIDPChangedEvent) case *org.GoogleIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) case *org.GoogleIDPChangedEvent: @@ -235,11 +308,14 @@ func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { AggregateTypes(org.AggregateType). AggregateIDs(wm.AggregateID). EventTypes( + org.OAuthIDPAddedEventType, + org.OAuthIDPChangedEventType, org.GoogleIDPAddedEventType, org.GoogleIDPChangedEventType, org.LDAPIDPAddedEventType, org.LDAPIDPChangedEventType, org.IDPRemovedEventType, ). + EventData(map[string]interface{}{"id": wm.ID}). Builder() } diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index b3bc1c4a7..0fcb4c6d6 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -18,6 +18,552 @@ import ( "github.com/zitadel/zitadel/internal/repository/org" ) +func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + provider GenericOAuthProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{ + Name: "name", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientSecret", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid auth endpoint", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid token endpoint", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid user endpoint", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + eventPusherToEvents( + org.NewOAuthIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + nil, + idp.Options{}, + )), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "ok all set", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + eventPusherToEvents( + org.NewOAuthIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + []string{"user"}, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + )), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + Scopes: []string{"user"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + idpConfigEncryption: tt.fields.secretCrypto, + } + id, got, err := c.AddOrgGenericOAuthProvider(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, id) + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + id string + provider GenericOAuthProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GenericOAuthProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid auth endpoint", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid token endpoint", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid user endpoint", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewOAuthIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + nil, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{ + Name: "name", + ClientID: "clientID", + AuthorizationEndpoint: "auth", + TokenEndpoint: "token", + UserEndpoint: "user", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewOAuthIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + nil, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + func() eventstore.Command { + t := true + event, _ := org.NewOAuthIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.OAuthIDPChanges{ + idp.ChangeOAuthName("new name"), + idp.ChangeOAuthClientID("clientID2"), + idp.ChangeOAuthClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + idp.ChangeOAuthAuthorizationEndpoint("new auth"), + idp.ChangeOAuthTokenEndpoint("new token"), + idp.ChangeOAuthUserEndpoint("new user"), + idp.ChangeOAuthScopes([]string{"openid", "profile"}), + idp.ChangeOAuthOptions(idp.OptionChanges{ + IsCreationAllowed: &t, + IsLinkingAllowed: &t, + IsAutoCreation: &t, + IsAutoUpdate: &t, + }), + }, + ) + return event + }(), + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GenericOAuthProvider{ + Name: "new name", + ClientID: "clientID2", + ClientSecret: "newSecret", + AuthorizationEndpoint: "new auth", + TokenEndpoint: "new token", + UserEndpoint: "new user", + Scopes: []string{"openid", "profile"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + 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, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.UpdateOrgGenericOAuthProvider(tt.args.ctx, tt.args.resourceOwner, 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 { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + func TestCommandSide_AddOrgGoogleIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -283,7 +829,7 @@ func TestCommandSide_UpdateOrgGoogleIDP(t *testing.T) { }, }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ResourceOwner: "org1"}, }, }, { @@ -907,7 +1453,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { }, }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ResourceOwner: "org1"}, }, }, { diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index d99b8a8bd..6f992fef8 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -33,6 +33,7 @@ type IDPTemplate struct { IsLinkingAllowed bool IsAutoCreation bool IsAutoUpdate bool + *OAuthIDPTemplate *GoogleIDPTemplate *LDAPIDPTemplate } @@ -42,6 +43,16 @@ type IDPTemplates struct { Templates []*IDPTemplate } +type OAuthIDPTemplate struct { + IDPID string + ClientID string + ClientSecret *crypto.CryptoValue + AuthorizationEndpoint string + TokenEndpoint string + UserEndpoint string + Scopes database.StringArray +} + type GoogleIDPTemplate struct { IDPID string ClientID string @@ -129,6 +140,45 @@ var ( } ) +var ( + oauthIdpTemplateTable = table{ + name: projection.IDPTemplateOAuthTable, + instanceIDCol: projection.OAuthInstanceIDCol, + } + OAuthIDCol = Column{ + name: projection.OAuthIDCol, + table: oauthIdpTemplateTable, + } + OAuthInstanceIDCol = Column{ + name: projection.OAuthInstanceIDCol, + table: oauthIdpTemplateTable, + } + OAuthClientIDCol = Column{ + name: projection.OAuthClientIDCol, + table: oauthIdpTemplateTable, + } + OAuthClientSecretCol = Column{ + name: projection.OAuthClientSecretCol, + table: oauthIdpTemplateTable, + } + OAuthAuthorizationEndpointCol = Column{ + name: projection.OAuthAuthorizationEndpointCol, + table: oauthIdpTemplateTable, + } + OAuthTokenEndpointCol = Column{ + name: projection.OAuthTokenEndpointCol, + table: oauthIdpTemplateTable, + } + OAuthUserEndpointCol = Column{ + name: projection.OAuthUserEndpointCol, + table: oauthIdpTemplateTable, + } + OAuthScopesCol = Column{ + name: projection.OAuthScopesCol, + table: oauthIdpTemplateTable, + } +) + var ( googleIdpTemplateTable = table{ name: projection.IDPTemplateGoogleTable, @@ -370,10 +420,20 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla IDPTemplateIsLinkingAllowedCol.identifier(), IDPTemplateIsAutoCreationCol.identifier(), IDPTemplateIsAutoUpdateCol.identifier(), + // oauth + OAuthIDCol.identifier(), + OAuthClientIDCol.identifier(), + OAuthClientSecretCol.identifier(), + OAuthAuthorizationEndpointCol.identifier(), + OAuthTokenEndpointCol.identifier(), + OAuthUserEndpointCol.identifier(), + OAuthScopesCol.identifier(), + // google GoogleIDCol.identifier(), GoogleClientIDCol.identifier(), GoogleClientSecretCol.identifier(), GoogleScopesCol.identifier(), + // ldap LDAPIDCol.identifier(), LDAPHostCol.identifier(), LDAPPortCol.identifier(), @@ -397,6 +457,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla LDAPAvatarURLAttributeCol.identifier(), LDAPProfileAttributeCol.identifier(), ).From(idpTemplateTable.identifier()). + LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)). LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), @@ -405,6 +466,14 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla name := sql.NullString{} + oauthID := sql.NullString{} + oauthClientID := sql.NullString{} + oauthClientSecret := new(crypto.CryptoValue) + oauthAuthorizationEndpoint := sql.NullString{} + oauthTokenEndpoint := sql.NullString{} + oauthUserEndpoint := sql.NullString{} + oauthScopes := database.StringArray{} + googleID := sql.NullString{} googleClientID := sql.NullString{} googleClientSecret := new(crypto.CryptoValue) @@ -447,10 +516,20 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla &idpTemplate.IsLinkingAllowed, &idpTemplate.IsAutoCreation, &idpTemplate.IsAutoUpdate, + // oauth + &oauthID, + &oauthClientID, + &oauthClientSecret, + &oauthAuthorizationEndpoint, + &oauthTokenEndpoint, + &oauthUserEndpoint, + &oauthScopes, + // google &googleID, &googleClientID, &googleClientSecret, &googleScopes, + // ldap &ldapID, &ldapHost, &ldapPort, @@ -483,6 +562,17 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla idpTemplate.Name = name.String + if oauthID.Valid { + idpTemplate.OAuthIDPTemplate = &OAuthIDPTemplate{ + IDPID: oauthID.String, + ClientID: oauthClientID.String, + ClientSecret: oauthClientSecret, + AuthorizationEndpoint: oauthAuthorizationEndpoint.String, + TokenEndpoint: oauthTokenEndpoint.String, + UserEndpoint: oauthUserEndpoint.String, + Scopes: oauthScopes, + } + } if googleID.Valid { idpTemplate.GoogleIDPTemplate = &GoogleIDPTemplate{ IDPID: googleID.String, @@ -490,7 +580,8 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla ClientSecret: googleClientSecret, Scopes: googleScopes, } - } else if ldapID.Valid { + } + if ldapID.Valid { idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{ IDPID: ldapID.String, Host: ldapHost.String, @@ -538,10 +629,20 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate IDPTemplateIsLinkingAllowedCol.identifier(), IDPTemplateIsAutoCreationCol.identifier(), IDPTemplateIsAutoUpdateCol.identifier(), + // oauth + OAuthIDCol.identifier(), + OAuthClientIDCol.identifier(), + OAuthClientSecretCol.identifier(), + OAuthAuthorizationEndpointCol.identifier(), + OAuthTokenEndpointCol.identifier(), + OAuthUserEndpointCol.identifier(), + OAuthScopesCol.identifier(), + // google GoogleIDCol.identifier(), GoogleClientIDCol.identifier(), GoogleClientSecretCol.identifier(), GoogleScopesCol.identifier(), + // ldap LDAPIDCol.identifier(), LDAPHostCol.identifier(), LDAPPortCol.identifier(), @@ -566,6 +667,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate LDAPProfileAttributeCol.identifier(), countColumn.identifier(), ).From(idpTemplateTable.identifier()). + LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)). LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), @@ -577,6 +679,14 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate name := sql.NullString{} + oauthID := sql.NullString{} + oauthClientID := sql.NullString{} + oauthClientSecret := new(crypto.CryptoValue) + oauthAuthorizationEndpoint := sql.NullString{} + oauthTokenEndpoint := sql.NullString{} + oauthUserEndpoint := sql.NullString{} + oauthScopes := database.StringArray{} + googleID := sql.NullString{} googleClientID := sql.NullString{} googleClientSecret := new(crypto.CryptoValue) @@ -619,10 +729,20 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate &idpTemplate.IsLinkingAllowed, &idpTemplate.IsAutoCreation, &idpTemplate.IsAutoUpdate, + // oauth + &oauthID, + &oauthClientID, + &oauthClientSecret, + &oauthAuthorizationEndpoint, + &oauthTokenEndpoint, + &oauthUserEndpoint, + &oauthScopes, + // google &googleID, &googleClientID, &googleClientSecret, &googleScopes, + // ldap &ldapID, &ldapHost, &ldapPort, @@ -654,6 +774,17 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate idpTemplate.Name = name.String + if oauthID.Valid { + idpTemplate.OAuthIDPTemplate = &OAuthIDPTemplate{ + IDPID: oauthID.String, + ClientID: oauthClientID.String, + ClientSecret: oauthClientSecret, + AuthorizationEndpoint: oauthAuthorizationEndpoint.String, + TokenEndpoint: oauthTokenEndpoint.String, + UserEndpoint: oauthUserEndpoint.String, + Scopes: oauthScopes, + } + } if googleID.Valid { idpTemplate.GoogleIDPTemplate = &GoogleIDPTemplate{ IDPID: googleID.String, @@ -661,7 +792,8 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate ClientSecret: googleClientSecret, Scopes: googleScopes, } - } else if ldapID.Valid { + } + if ldapID.Valid { idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{ IDPID: ldapID.String, Host: ldapHost.String, diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index e1a4167c1..2efe3b4d7 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -28,10 +28,20 @@ var ( ` projections.idp_templates.is_linking_allowed,` + ` projections.idp_templates.is_auto_creation,` + ` projections.idp_templates.is_auto_update,` + + // oauth + ` projections.idp_templates_oauth.idp_id,` + + ` projections.idp_templates_oauth.client_id,` + + ` projections.idp_templates_oauth.client_secret,` + + ` projections.idp_templates_oauth.authorization_endpoint,` + + ` projections.idp_templates_oauth.token_endpoint,` + + ` projections.idp_templates_oauth.user_endpoint,` + + ` projections.idp_templates_oauth.scopes,` + + // google ` projections.idp_templates_google.idp_id,` + ` projections.idp_templates_google.client_id,` + ` projections.idp_templates_google.client_secret,` + ` projections.idp_templates_google.scopes,` + + // ldap ` projections.idp_templates_ldap.idp_id,` + ` projections.idp_templates_ldap.host,` + ` projections.idp_templates_ldap.port,` + @@ -55,6 +65,7 @@ var ( ` projections.idp_templates_ldap.avatar_url_attribute,` + ` projections.idp_templates_ldap.profile_attribute` + ` FROM projections.idp_templates` + + ` LEFT JOIN projections.idp_templates_oauth ON projections.idp_templates.id = projections.idp_templates_oauth.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_oauth.instance_id` + ` LEFT JOIN projections.idp_templates_google ON projections.idp_templates.id = projections.idp_templates_google.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_google.instance_id` + ` LEFT JOIN projections.idp_templates_ldap ON projections.idp_templates.id = projections.idp_templates_ldap.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_ldap.instance_id` idpTemplateCols = []string{ @@ -71,6 +82,14 @@ var ( "is_linking_allowed", "is_auto_creation", "is_auto_update", + // oauth config + "idp_id", + "client_id", + "client_secret", + "authorization_endpoint", + "token_endpoint", + "user_endpoint", + "scopes", // google config "idp_id", "client_id", @@ -113,10 +132,20 @@ var ( ` projections.idp_templates.is_linking_allowed,` + ` projections.idp_templates.is_auto_creation,` + ` projections.idp_templates.is_auto_update,` + + // oauth + ` projections.idp_templates_oauth.idp_id,` + + ` projections.idp_templates_oauth.client_id,` + + ` projections.idp_templates_oauth.client_secret,` + + ` projections.idp_templates_oauth.authorization_endpoint,` + + ` projections.idp_templates_oauth.token_endpoint,` + + ` projections.idp_templates_oauth.user_endpoint,` + + ` projections.idp_templates_oauth.scopes,` + + // google ` projections.idp_templates_google.idp_id,` + ` projections.idp_templates_google.client_id,` + ` projections.idp_templates_google.client_secret,` + ` projections.idp_templates_google.scopes,` + + // ldap ` projections.idp_templates_ldap.idp_id,` + ` projections.idp_templates_ldap.host,` + ` projections.idp_templates_ldap.port,` + @@ -141,6 +170,7 @@ var ( ` projections.idp_templates_ldap.profile_attribute,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_templates` + + ` LEFT JOIN projections.idp_templates_oauth ON projections.idp_templates.id = projections.idp_templates_oauth.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_oauth.instance_id` + ` LEFT JOIN projections.idp_templates_google ON projections.idp_templates.id = projections.idp_templates_google.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_google.instance_id` + ` LEFT JOIN projections.idp_templates_ldap ON projections.idp_templates.id = projections.idp_templates_ldap.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_ldap.instance_id` idpTemplatesCols = []string{ @@ -157,6 +187,14 @@ var ( "is_linking_allowed", "is_auto_creation", "is_auto_update", + // oauth config + "idp_id", + "client_id", + "client_secret", + "authorization_endpoint", + "token_endpoint", + "user_endpoint", + "scopes", // google config "idp_id", "client_id", @@ -218,7 +256,91 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { }, object: (*IDPTemplate)(nil), }, - + { + name: "prepareIDPTemplateByIDQuery oauth idp", + prepare: prepareIDPTemplateByIDQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(idpTemplateQuery), + idpTemplateCols, + []driver.Value{ + "idp-id", + "ro", + testNow, + testNow, + uint64(20211109), + domain.IDPConfigStateActive, + "idp-name", + domain.IDPTypeOAuth, + domain.IdentityProviderTypeOrg, + true, + true, + true, + true, + // oauth + "idp-id", + "client_id", + nil, + "authorization", + "token", + "user", + database.StringArray{"profile"}, + // google + nil, + nil, + nil, + nil, + // ldap config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, + ), + }, + object: &IDPTemplate{ + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + ResourceOwner: "ro", + ID: "idp-id", + State: domain.IDPStateActive, + Name: "idp-name", + Type: domain.IDPTypeOAuth, + OwnerType: domain.IdentityProviderTypeOrg, + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + OAuthIDPTemplate: &OAuthIDPTemplate{ + IDPID: "idp-id", + ClientID: "client_id", + ClientSecret: nil, + AuthorizationEndpoint: "authorization", + TokenEndpoint: "token", + UserEndpoint: "user", + Scopes: []string{"profile"}, + }, + }, + }, { name: "prepareIDPTemplateByIDQuery google idp", prepare: prepareIDPTemplateByIDQuery, @@ -240,6 +362,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, // google "idp-id", "client_id", @@ -314,6 +444,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, // google nil, nil, @@ -407,6 +545,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, // google config nil, nil, @@ -511,6 +657,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, // google config nil, nil, @@ -613,6 +767,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, // google config nil, nil, @@ -690,6 +852,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, // google config nil, nil, @@ -733,6 +903,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, // google "idp-id-google", "client_id", @@ -762,12 +940,63 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, }, + { + "idp-id-oauth", + "ro", + testNow, + testNow, + uint64(20211109), + domain.IDPConfigStateActive, + "idp-name", + domain.IDPTypeOAuth, + domain.IdentityProviderTypeOrg, + true, + true, + true, + true, + // oauth + "idp-id-oauth", + "client_id", + nil, + "authorization", + "token", + "user", + database.StringArray{"profile"}, + // google + nil, + nil, + nil, + nil, + // ldap config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, }, ), }, object: &IDPTemplates{ SearchResponse: SearchResponse{ - Count: 2, + Count: 3, }, Templates: []*IDPTemplate{ { @@ -831,6 +1060,31 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { Scopes: []string{"profile"}, }, }, + + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + ResourceOwner: "ro", + ID: "idp-id-oauth", + State: domain.IDPStateActive, + Name: "idp-name", + Type: domain.IDPTypeOAuth, + OwnerType: domain.IdentityProviderTypeOrg, + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + OAuthIDPTemplate: &OAuthIDPTemplate{ + IDPID: "idp-id-oauth", + ClientID: "client_id", + ClientSecret: nil, + AuthorizationEndpoint: "authorization", + TokenEndpoint: "token", + UserEndpoint: "user", + Scopes: []string{"profile"}, + }, + }, }, }, }, diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index 1c7c3750a..4ef51450c 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -17,9 +17,11 @@ import ( const ( IDPTemplateTable = "projections.idp_templates" + IDPTemplateOAuthTable = IDPTemplateTable + "_" + IDPTemplateOAuthSuffix IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix + IDPTemplateOAuthSuffix = "oauth" IDPTemplateGoogleSuffix = "google" IDPTemplateLDAPSuffix = "ldap" @@ -39,6 +41,15 @@ const ( IDPTemplateIsAutoCreationCol = "is_auto_creation" IDPTemplateIsAutoUpdateCol = "is_auto_update" + OAuthIDCol = "idp_id" + OAuthInstanceIDCol = "instance_id" + OAuthClientIDCol = "client_id" + OAuthClientSecretCol = "client_secret" + OAuthAuthorizationEndpointCol = "authorization_endpoint" + OAuthTokenEndpointCol = "token_endpoint" + OAuthUserEndpointCol = "user_endpoint" + OAuthScopesCol = "scopes" + GoogleIDCol = "idp_id" GoogleInstanceIDCol = "instance_id" GoogleClientIDCol = "client_id" @@ -100,6 +111,20 @@ func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerC crdb.WithIndex(crdb.NewIndex("resource_owner", []string{IDPTemplateResourceOwnerCol})), crdb.WithIndex(crdb.NewIndex("owner_removed", []string{IDPTemplateOwnerRemovedCol})), ), + crdb.NewSuffixedTable([]*crdb.Column{ + crdb.NewColumn(OAuthIDCol, crdb.ColumnTypeText), + crdb.NewColumn(OAuthInstanceIDCol, crdb.ColumnTypeText), + crdb.NewColumn(OAuthClientIDCol, crdb.ColumnTypeText), + crdb.NewColumn(OAuthClientSecretCol, crdb.ColumnTypeJSONB), + crdb.NewColumn(OAuthAuthorizationEndpointCol, crdb.ColumnTypeText), + crdb.NewColumn(OAuthTokenEndpointCol, crdb.ColumnTypeText), + crdb.NewColumn(OAuthUserEndpointCol, crdb.ColumnTypeText), + crdb.NewColumn(OAuthScopesCol, crdb.ColumnTypeTextArray, crdb.Nullable()), + }, + crdb.NewPrimaryKey(OAuthInstanceIDCol, OAuthIDCol), + IDPTemplateOAuthSuffix, + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()), + ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(GoogleIDCol, crdb.ColumnTypeText), crdb.NewColumn(GoogleInstanceIDCol, crdb.ColumnTypeText), @@ -150,6 +175,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { { Aggregate: instance.AggregateType, EventRedusers: []handler.EventReducer{ + { + Event: instance.OAuthIDPAddedEventType, + Reduce: p.reduceOAuthIDPAdded, + }, + { + Event: instance.OAuthIDPChangedEventType, + Reduce: p.reduceOAuthIDPChanged, + }, { Event: instance.GoogleIDPAddedEventType, Reduce: p.reduceGoogleIDPAdded, @@ -179,6 +212,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { { Aggregate: org.AggregateType, EventRedusers: []handler.EventReducer{ + { + Event: org.OAuthIDPAddedEventType, + Reduce: p.reduceOAuthIDPAdded, + }, + { + Event: org.OAuthIDPChangedEventType, + Reduce: p.reduceOAuthIDPChanged, + }, { Event: org.GoogleIDPAddedEventType, Reduce: p.reduceGoogleIDPAdded, @@ -208,6 +249,97 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { } } +func (p *idpTemplateProjection) reduceOAuthIDPAdded(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.OAuthIDPAddedEvent + var idpOwnerType domain.IdentityProviderType + switch e := event.(type) { + case *org.OAuthIDPAddedEvent: + idpEvent = e.OAuthIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeOrg + case *instance.OAuthIDPAddedEvent: + idpEvent = e.OAuthIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeSystem + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ap9ihb", "reduce.wrong.event.type %v", []eventstore.EventType{org.OAuthIDPAddedEventType, instance.OAuthIDPAddedEventType}) + } + + return crdb.NewMultiStatement( + &idpEvent, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(IDPTemplateIDCol, idpEvent.ID), + handler.NewCol(IDPTemplateCreationDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateResourceOwnerCol, idpEvent.Aggregate().ResourceOwner), + handler.NewCol(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive), + handler.NewCol(IDPTemplateNameCol, idpEvent.Name), + handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeOAuth), + handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed), + handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed), + handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation), + handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate), + }, + ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(OAuthIDCol, idpEvent.ID), + handler.NewCol(OAuthInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(OAuthClientIDCol, idpEvent.ClientID), + handler.NewCol(OAuthClientSecretCol, idpEvent.ClientSecret), + handler.NewCol(OAuthAuthorizationEndpointCol, idpEvent.AuthorizationEndpoint), + handler.NewCol(OAuthTokenEndpointCol, idpEvent.TokenEndpoint), + handler.NewCol(OAuthUserEndpointCol, idpEvent.UserEndpoint), + handler.NewCol(OAuthScopesCol, database.StringArray(idpEvent.Scopes)), + }, + crdb.WithTableSuffix(IDPTemplateOAuthSuffix), + ), + ), nil +} + +func (p *idpTemplateProjection) reduceOAuthIDPChanged(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.OAuthIDPChangedEvent + switch e := event.(type) { + case *org.OAuthIDPChangedEvent: + idpEvent = e.OAuthIDPChangedEvent + case *instance.OAuthIDPChangedEvent: + idpEvent = e.OAuthIDPChangedEvent + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.OAuthIDPChangedEventType, instance.OAuthIDPChangedEventType}) + } + + ops := make([]func(eventstore.Event) crdb.Exec, 0, 2) + ops = append(ops, + crdb.AddUpdateStatement( + reduceIDPChangedTemplateColumns(idpEvent.Name, idpEvent.CreationDate(), idpEvent.Sequence(), idpEvent.OptionChanges), + []handler.Condition{ + handler.NewCond(IDPTemplateIDCol, idpEvent.ID), + handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + ), + ) + oauthCols := reduceOAuthIDPChangedColumns(idpEvent) + if len(oauthCols) > 0 { + ops = append(ops, + crdb.AddUpdateStatement( + oauthCols, + []handler.Condition{ + handler.NewCond(OAuthIDCol, idpEvent.ID), + handler.NewCond(OAuthInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(IDPTemplateOAuthSuffix), + ), + ) + } + + return crdb.NewMultiStatement( + &idpEvent, + ops..., + ), nil +} + func (p *idpTemplateProjection) reduceGoogleIDPAdded(event eventstore.Event) (*handler.Statement, error) { var idpEvent idp.GoogleIDPAddedEvent var idpOwnerType domain.IdentityProviderType @@ -466,6 +598,29 @@ func reduceIDPChangedTemplateColumns(name *string, creationDate time.Time, seque ) } +func reduceOAuthIDPChangedColumns(idpEvent idp.OAuthIDPChangedEvent) []handler.Column { + oauthCols := make([]handler.Column, 0, 6) + if idpEvent.ClientID != nil { + oauthCols = append(oauthCols, handler.NewCol(OAuthClientIDCol, *idpEvent.ClientID)) + } + if idpEvent.ClientSecret != nil { + oauthCols = append(oauthCols, handler.NewCol(OAuthClientSecretCol, *idpEvent.ClientSecret)) + } + if idpEvent.AuthorizationEndpoint != nil { + oauthCols = append(oauthCols, handler.NewCol(OAuthAuthorizationEndpointCol, *idpEvent.AuthorizationEndpoint)) + } + if idpEvent.TokenEndpoint != nil { + oauthCols = append(oauthCols, handler.NewCol(OAuthTokenEndpointCol, *idpEvent.TokenEndpoint)) + } + if idpEvent.UserEndpoint != nil { + oauthCols = append(oauthCols, handler.NewCol(OAuthUserEndpointCol, *idpEvent.UserEndpoint)) + } + if idpEvent.Scopes != nil { + oauthCols = append(oauthCols, handler.NewCol(OAuthScopesCol, database.StringArray(idpEvent.Scopes))) + } + return oauthCols +} + func reduceGoogleIDPChangedColumns(idpEvent idp.GoogleIDPChangedEvent) []handler.Column { googleCols := make([]handler.Column, 0, 3) if idpEvent.ClientID != nil { diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index 7de4bf9a2..b7b53ff4f 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -125,6 +125,276 @@ func TestIDPTemplateProjection_reducesRemove(t *testing.T) { } } +func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "instance reduceOAuthIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.OAuthIDPAddedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "custom-zitadel-instance", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "authorizationEndpoint": "auth", + "tokenEndpoint": "token", + "userEndpoint": "user", + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.OAuthIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOAuthIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.idp_templates (id, creation_date, change_date, sequence, resource_owner, instance_id, state, name, owner_type, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "custom-zitadel-instance", + domain.IdentityProviderTypeSystem, + domain.IDPTypeOAuth, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates_oauth (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + "auth", + "token", + "user", + database.StringArray{"profile"}, + }, + }, + }, + }, + }, + }, + { + name: "org reduceOAuthIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.OAuthIDPAddedEventType), + org.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "custom-zitadel-instance", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "authorizationEndpoint": "auth", + "tokenEndpoint": "token", + "userEndpoint": "user", + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), org.OAuthIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOAuthIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.idp_templates (id, creation_date, change_date, sequence, resource_owner, instance_id, state, name, owner_type, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "custom-zitadel-instance", + domain.IdentityProviderTypeOrg, + domain.IDPTypeOAuth, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates_oauth (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + "auth", + "token", + "user", + database.StringArray{"profile"}, + }, + }, + }, + }, + }, + }, + { + name: "instance reduceOAuthIDPChanged minimal", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.OAuthIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "isCreationAllowed": true, + "client_id": "id" +}`), + ), instance.OAuthIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOAuthIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates SET (is_creation_allowed, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + true, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates_oauth SET client_id = $1 WHERE (idp_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "id", + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceOAuthIDPChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.OAuthIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "custom-zitadel-instance", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "authorizationEndpoint": "auth", + "tokenEndpoint": "token", + "userEndpoint": "user", + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.OAuthIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOAuthIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates SET (name, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update, change_date, sequence) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (instance_id = $9)", + expectedArgs: []interface{}{ + "custom-zitadel-instance", + true, + true, + true, + true, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates_oauth SET (client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes) = ($1, $2, $3, $4, $5, $6) WHERE (idp_id = $7) AND (instance_id = $8)", + expectedArgs: []interface{}{ + "client_id", + anyArg{}, + "auth", + "token", + "user", + database.StringArray{"profile"}, + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if !errors.IsErrorInvalidArgument(err) { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, IDPTemplateTable, tt.want) + }) + } +} + func TestIDPTemplateProjection_reducesGoogle(t *testing.T) { type args struct { event func(t *testing.T) eventstore.Event diff --git a/internal/repository/idp/idp.go b/internal/repository/idp/idp.go index 4b1fa9d81..a2d1a7b98 100644 --- a/internal/repository/idp/idp.go +++ b/internal/repository/idp/idp.go @@ -47,8 +47,8 @@ func (o *Options) ReduceChanges(changes OptionChanges) { if changes.IsLinkingAllowed != nil { o.IsLinkingAllowed = *changes.IsLinkingAllowed } - if changes.IsAutoUpdate != nil { - o.IsAutoUpdate = *changes.IsAutoUpdate + if changes.IsAutoCreation != nil { + o.IsAutoCreation = *changes.IsAutoCreation } if changes.IsAutoUpdate != nil { o.IsAutoUpdate = *changes.IsAutoUpdate diff --git a/internal/repository/idp/oauth.go b/internal/repository/idp/oauth.go new file mode 100644 index 000000000..6fa6979cc --- /dev/null +++ b/internal/repository/idp/oauth.go @@ -0,0 +1,173 @@ +package idp + +import ( + "encoding/json" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" +) + +type OAuthIDPAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name string `json:"name,omitempty"` + ClientID string `json:"client_id,omitempty"` + ClientSecret *crypto.CryptoValue `json:"client_secret,omitempty"` + AuthorizationEndpoint string `json:"authorizationEndpoint,omitempty"` + TokenEndpoint string `json:"tokenEndpoint,omitempty"` + UserEndpoint string `json:"userEndpoint,omitempty"` + Scopes []string `json:"scopes,omitempty"` + Options +} + +func NewOAuthIDPAddedEvent( + base *eventstore.BaseEvent, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + authorizationEndpoint, + tokenEndpoint, + userEndpoint string, + scopes []string, + options Options, +) *OAuthIDPAddedEvent { + return &OAuthIDPAddedEvent{ + BaseEvent: *base, + ID: id, + Name: name, + ClientID: clientID, + ClientSecret: clientSecret, + AuthorizationEndpoint: authorizationEndpoint, + TokenEndpoint: tokenEndpoint, + UserEndpoint: userEndpoint, + Scopes: scopes, + Options: options, + } +} + +func (e *OAuthIDPAddedEvent) Data() interface{} { + return e +} + +func (e *OAuthIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func OAuthIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &OAuthIDPAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-Et1dq", "unable to unmarshal event") + } + + return e, nil +} + +type OAuthIDPChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name *string `json:"name,omitempty"` + ClientID *string `json:"client_id,omitempty"` + ClientSecret *crypto.CryptoValue `json:"client_secret,omitempty"` + AuthorizationEndpoint *string `json:"authorizationEndpoint,omitempty"` + TokenEndpoint *string `json:"tokenEndpoint,omitempty"` + UserEndpoint *string `json:"userEndpoint,omitempty"` + Scopes []string `json:"scopes,omitempty"` + OptionChanges +} + +func NewOAuthIDPChangedEvent( + base *eventstore.BaseEvent, + id string, + changes []OAuthIDPChanges, +) (*OAuthIDPChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "IDP-BH3dl", "Errors.NoChangesFound") + } + changedEvent := &OAuthIDPChangedEvent{ + BaseEvent: *base, + ID: id, + } + for _, change := range changes { + change(changedEvent) + } + return changedEvent, nil +} + +type OAuthIDPChanges func(*OAuthIDPChangedEvent) + +func ChangeOAuthName(name string) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.Name = &name + } +} +func ChangeOAuthClientID(clientID string) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.ClientID = &clientID + } +} + +func ChangeOAuthClientSecret(clientSecret *crypto.CryptoValue) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.ClientSecret = clientSecret + } +} + +func ChangeOAuthOptions(options OptionChanges) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.OptionChanges = options + } +} + +func ChangeOAuthAuthorizationEndpoint(authorizationEndpoint string) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.AuthorizationEndpoint = &authorizationEndpoint + } +} + +func ChangeOAuthTokenEndpoint(tokenEndpoint string) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.TokenEndpoint = &tokenEndpoint + } +} + +func ChangeOAuthUserEndpoint(userEndpoint string) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.UserEndpoint = &userEndpoint + } +} + +func ChangeOAuthScopes(scopes []string) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.Scopes = scopes + } +} + +func (e *OAuthIDPChangedEvent) Data() interface{} { + return e +} + +func (e *OAuthIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func OAuthIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &OAuthIDPChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-SAf3gw", "unable to unmarshal event") + } + + return e, nil +} diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 18fd6cff1..66ee70f54 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -70,6 +70,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper). RegisterFilterEventMapper(AggregateType, IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper). RegisterFilterEventMapper(AggregateType, IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper). + RegisterFilterEventMapper(AggregateType, OAuthIDPAddedEventType, OAuthIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, OAuthIDPChangedEventType, OAuthIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GoogleIDPAddedEventType, GoogleIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper). diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index cb580cbe6..d8d390a09 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -10,6 +10,8 @@ import ( ) const ( + OAuthIDPAddedEventType eventstore.EventType = "instance.idp.oauth.added" + OAuthIDPChangedEventType eventstore.EventType = "instance.idp.oauth.changed" GoogleIDPAddedEventType eventstore.EventType = "instance.idp.google.added" GoogleIDPChangedEventType eventstore.EventType = "instance.idp.google.changed" LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.added" @@ -17,6 +19,88 @@ const ( IDPRemovedEventType eventstore.EventType = "instance.idp.removed" ) +type OAuthIDPAddedEvent struct { + idp.OAuthIDPAddedEvent +} + +func NewOAuthIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + authorizationEndpoint, + tokenEndpoint, + userEndpoint string, + scopes []string, + options idp.Options, +) *OAuthIDPAddedEvent { + + return &OAuthIDPAddedEvent{ + OAuthIDPAddedEvent: *idp.NewOAuthIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OAuthIDPAddedEventType, + ), + id, + name, + clientID, + clientSecret, + authorizationEndpoint, + tokenEndpoint, + userEndpoint, + scopes, + options, + ), + } +} + +func OAuthIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OAuthIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &OAuthIDPAddedEvent{OAuthIDPAddedEvent: *e.(*idp.OAuthIDPAddedEvent)}, nil +} + +type OAuthIDPChangedEvent struct { + idp.OAuthIDPChangedEvent +} + +func NewOAuthIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.OAuthIDPChanges, +) (*OAuthIDPChangedEvent, error) { + + changedEvent, err := idp.NewOAuthIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OAuthIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &OAuthIDPChangedEvent{OAuthIDPChangedEvent: *changedEvent}, nil +} + +func OAuthIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OAuthIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &OAuthIDPChangedEvent{OAuthIDPChangedEvent: *e.(*idp.OAuthIDPChangedEvent)}, nil +} + type GoogleIDPAddedEvent struct { idp.GoogleIDPAddedEvent } diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index f3d7f86d5..5652a2aa7 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -78,6 +78,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper). RegisterFilterEventMapper(AggregateType, IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper). RegisterFilterEventMapper(AggregateType, IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper). + RegisterFilterEventMapper(AggregateType, OAuthIDPAddedEventType, OAuthIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, OAuthIDPChangedEventType, OAuthIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GoogleIDPAddedEventType, GoogleIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper). diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index c41e8537d..d360e2cb2 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -10,6 +10,8 @@ import ( ) const ( + OAuthIDPAddedEventType eventstore.EventType = "org.idp.oauth.added" + OAuthIDPChangedEventType eventstore.EventType = "org.idp.oauth.changed" GoogleIDPAddedEventType eventstore.EventType = "org.idp.google.added" GoogleIDPChangedEventType eventstore.EventType = "org.idp.google.changed" LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added" @@ -17,6 +19,88 @@ const ( IDPRemovedEventType eventstore.EventType = "org.idp.removed" ) +type OAuthIDPAddedEvent struct { + idp.OAuthIDPAddedEvent +} + +func NewOAuthIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + authorizationEndpoint, + tokenEndpoint, + userEndpoint string, + scopes []string, + options idp.Options, +) *OAuthIDPAddedEvent { + + return &OAuthIDPAddedEvent{ + OAuthIDPAddedEvent: *idp.NewOAuthIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OAuthIDPAddedEventType, + ), + id, + name, + clientID, + clientSecret, + authorizationEndpoint, + tokenEndpoint, + userEndpoint, + scopes, + options, + ), + } +} + +func OAuthIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OAuthIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &OAuthIDPAddedEvent{OAuthIDPAddedEvent: *e.(*idp.OAuthIDPAddedEvent)}, nil +} + +type OAuthIDPChangedEvent struct { + idp.OAuthIDPChangedEvent +} + +func NewOAuthIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.OAuthIDPChanges, +) (*OAuthIDPChangedEvent, error) { + + changedEvent, err := idp.NewOAuthIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OAuthIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &OAuthIDPChangedEvent{OAuthIDPChangedEvent: *changedEvent}, nil +} + +func OAuthIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OAuthIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &OAuthIDPChangedEvent{OAuthIDPChangedEvent: *e.(*idp.OAuthIDPChangedEvent)}, nil +} + type GoogleIDPAddedEvent struct { idp.GoogleIDPAddedEvent } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 98d9f96a7..895862681 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1248,6 +1248,30 @@ service AdminService { }; } + // Add a new OAuth2 identity provider on the instance + rpc AddGenericOAuthProvider(AddGenericOAuthProviderRequest) returns (AddGenericOAuthProviderResponse) { + option (google.api.http) = { + post: "/idps/oauth" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + } + + // Change an existing OAuth2 identity provider on the instance + rpc UpdateGenericOAuthProvider(UpdateGenericOAuthProviderRequest) returns (UpdateGenericOAuthProviderResponse) { + option (google.api.http) = { + put: "/idps/oauth/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + } + // Add a new Google identity provider on the instance rpc AddGoogleProvider(AddGoogleProviderRequest) returns (AddGoogleProviderResponse) { option (google.api.http) = { @@ -4259,6 +4283,39 @@ message GetProviderByIDResponse { zitadel.idp.v1.Provider idp = 1; } +message AddGenericOAuthProviderRequest { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_secret = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string authorization_endpoint = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string token_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string user_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 8; +} + +message AddGenericOAuthProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateGenericOAuthProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // client_secret will only be updated if provided + string client_secret = 4 [(validate.rules).string = {max_len: 200}]; + string authorization_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string token_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string user_endpoint = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string scopes = 8 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 9; +} + +message UpdateGenericOAuthProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddGoogleProviderRequest { // Google will be used as default, if no name is provided string name = 1 [(validate.rules).string = {max_len: 200}]; diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 88adbcff8..fdfe2edaf 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -263,8 +263,16 @@ message ProviderConfig { oneof config { LDAPConfig ldap = 2; GoogleConfig google = 3; + OAuthConfig oauth = 4; } } +message OAuthConfig { + string client_id = 1; + string authorization_endpoint = 2; + string token_endpoint = 3; + string user_endpoint = 4; + repeated string scopes = 5; +} message GoogleConfig { string client_id = 1; diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 79df26c7a..ba3f02be9 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -4369,6 +4369,29 @@ service ManagementService { }; } + // Add a new OAuth2 identity provider in the organisation + rpc AddGenericOAuthProvider(AddGenericOAuthProviderRequest) returns (AddGenericOAuthProviderResponse) { + option (google.api.http) = { + post: "/idps/oauth" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + } + + // Change an existing OAuth2 identity provider in the organisation + rpc UpdateGenericOAuthProvider(UpdateGenericOAuthProviderRequest) returns (UpdateGenericOAuthProviderResponse) { + option (google.api.http) = { + put: "/idps/oauth/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + } // Add a new Google identity provider in the organisation rpc AddGoogleProvider(AddGoogleProviderRequest) returns (AddGoogleProviderResponse) { @@ -7874,6 +7897,39 @@ message GetProviderByIDResponse { zitadel.idp.v1.Provider idp = 1; } +message AddGenericOAuthProviderRequest { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_secret = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string authorization_endpoint = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string token_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string user_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 8; +} + +message AddGenericOAuthProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateGenericOAuthProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // client_secret will only be updated if provided + string client_secret = 4 [(validate.rules).string = {max_len: 200}]; + string authorization_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string token_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string user_endpoint = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string scopes = 8 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 9; +} + +message UpdateGenericOAuthProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddGoogleProviderRequest { // Google will be used as default, if no name is provided string name = 1 [(validate.rules).string = {max_len: 200}];