diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index e48ccd622..797bf1036 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -593,12 +593,36 @@ Returns an identity provider of the instance GET: /idps/templates/{id} +### AddGoogleProvider + +> **rpc** AddGoogleProvider([AddGoogleProviderRequest](#addgoogleproviderrequest)) +[AddGoogleProviderResponse](#addgoogleproviderresponse) + +Add a new Google identity provider on the instance + + + + POST: /idps/google + + +### UpdateGoogleProvider + +> **rpc** UpdateGoogleProvider([UpdateGoogleProviderRequest](#updategoogleproviderrequest)) +[UpdateGoogleProviderResponse](#updategoogleproviderresponse) + +Change an existing Google identity provider on the instance + + + + PUT: /idps/google/{id} + + ### AddLDAPProvider > **rpc** AddLDAPProvider([AddLDAPProviderRequest](#addldapproviderrequest)) [AddLDAPProviderResponse](#addldapproviderresponse) -Add a new ldap identity provider on the instance +Add a new LDAP identity provider on the instance @@ -610,7 +634,7 @@ Add a new ldap identity provider on the instance > **rpc** UpdateLDAPProvider([UpdateLDAPProviderRequest](#updateldapproviderrequest)) [UpdateLDAPProviderResponse](#updateldapproviderresponse) -Change an existing ldap identity provider on the instance +Change an existing LDAP identity provider on the instance @@ -1849,6 +1873,33 @@ This is an empty request +### AddGoogleProviderRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| name | string | Google will be used as default, if no name is provided | string.max_len: 200
| +| client_id | string | - | string.min_len: 1
string.max_len: 200
| +| client_secret | string | - | string.min_len: 1
string.max_len: 200
| +| scopes | repeated string | - | repeated.max_items: 20
repeated.items.string.min_len: 1
repeated.items.string.max_len: 100
| +| provider_options | zitadel.idp.v1.Options | - | | + + + + +### AddGoogleProviderResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| id | string | - | | + + + + ### AddIAMMemberRequest @@ -4788,6 +4839,33 @@ this is en empty request +### UpdateGoogleProviderRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | string.min_len: 1
string.max_len: 200
| +| name | string | - | string.max_len: 200
| +| client_id | string | - | string.min_len: 1
string.max_len: 200
| +| client_secret | string | client_secret will only be updated if provided | string.max_len: 200
| +| scopes | repeated string | - | repeated.max_items: 20
repeated.items.string.min_len: 1
repeated.items.string.max_len: 100
| +| provider_options | zitadel.idp.v1.Options | - | | + + + + +### UpdateGoogleProviderResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateIAMMemberRequest diff --git a/docs/docs/apis/proto/idp.md b/docs/docs/apis/proto/idp.md index cf923573b..227950218 100644 --- a/docs/docs/apis/proto/idp.md +++ b/docs/docs/apis/proto/idp.md @@ -9,6 +9,18 @@ title: zitadel/idp.proto ## Messages +### GoogleConfig + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| client_id | string | - | | +| scopes | repeated string | - | | + + + + ### IDP @@ -201,6 +213,7 @@ title: zitadel/idp.proto | ----- | ---- | ----------- | ----------- | | options | Options | - | | | [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.ldap | LDAPConfig | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.google | GoogleConfig | - | | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index cfcac546f..2c93a965d 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -3050,12 +3050,36 @@ Returns an identity provider of the organisation GET: /idps/templates/{id} +### AddGoogleProvider + +> **rpc** AddGoogleProvider([AddGoogleProviderRequest](#addgoogleproviderrequest)) +[AddGoogleProviderResponse](#addgoogleproviderresponse) + +Add a new Google identity provider in the organisation + + + + POST: /idps/google + + +### UpdateGoogleProvider + +> **rpc** UpdateGoogleProvider([UpdateGoogleProviderRequest](#updategoogleproviderrequest)) +[UpdateGoogleProviderResponse](#updategoogleproviderresponse) + +Change an existing Google identity provider in the organisation + + + + PUT: /idps/google/{id} + + ### AddLDAPProvider > **rpc** AddLDAPProvider([AddLDAPProviderRequest](#addldapproviderrequest)) [AddLDAPProviderResponse](#addldapproviderresponse) -Add a new ldap identity provider in the organisation +Add a new LDAP identity provider in the organisation @@ -3067,7 +3091,7 @@ Add a new ldap identity provider in the organisation > **rpc** UpdateLDAPProvider([UpdateLDAPProviderRequest](#updateldapproviderrequest)) [UpdateLDAPProviderResponse](#updateldapproviderresponse) -Change an existing ldap identity provider in the organisation +Change an existing LDAP identity provider in the organisation @@ -3523,6 +3547,33 @@ This is an empty request +### AddGoogleProviderRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| name | string | Google will be used as default, if no name is provided | string.max_len: 200
| +| client_id | string | - | string.min_len: 1
string.max_len: 200
| +| client_secret | string | - | string.min_len: 1
string.max_len: 200
| +| scopes | repeated string | - | repeated.max_items: 20
repeated.items.string.min_len: 1
repeated.items.string.max_len: 100
| +| provider_options | zitadel.idp.v1.Options | - | | + + + + +### AddGoogleProviderResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| id | string | - | | + + + + ### AddHumanUserRequest @@ -8860,6 +8911,33 @@ This is an empty request +### UpdateGoogleProviderRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | string.min_len: 1
string.max_len: 200
| +| name | string | - | string.max_len: 200
| +| client_id | string | - | string.min_len: 1
string.max_len: 200
| +| client_secret | string | client_secret will only be updated if provided | string.max_len: 200
| +| scopes | repeated string | - | repeated.max_items: 20
repeated.items.string.min_len: 1
repeated.items.string.max_len: 100
| +| provider_options | zitadel.idp.v1.Options | - | | + + + + +### UpdateGoogleProviderResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateHumanEmailRequest diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index f0e73037b..5d3ca5700 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -174,6 +174,27 @@ func (s *Server) ListProviders(ctx context.Context, req *admin_pb.ListProvidersR }, 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)) + if err != nil { + return nil, err + } + return &admin_pb.AddGoogleProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +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)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateGoogleProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) AddLDAPProvider(ctx context.Context, req *admin_pb.AddLDAPProviderRequest) (*admin_pb.AddLDAPProviderResponse, error) { id, details, err := s.command.AddInstanceLDAPProvider(ctx, addLDAPProviderToCommand(req)) if err != nil { diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 43dac5e0d..1bf605088 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -201,6 +201,26 @@ func providerQueryToQuery(idpQuery *admin_pb.ProviderQuery) (query.SearchQuery, } } +func addGoogleProviderToCommand(req *admin_pb.AddGoogleProviderRequest) command.GoogleProvider { + return command.GoogleProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateGoogleProviderToCommand(req *admin_pb.UpdateGoogleProviderRequest) command.GoogleProvider { + return command.GoogleProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + func addLDAPProviderToCommand(req *admin_pb.AddLDAPProviderRequest) command.LDAPProvider { return command.LDAPProvider{ Name: req.Name, diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index 895a2ea97..9724703bf 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -402,6 +402,14 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig { IsAutoUpdate: config.IsAutoUpdate, }, } + if config.GoogleIDPTemplate != nil { + providerConfig.Config = &idp_pb.ProviderConfig_Google{ + Google: &idp_pb.GoogleConfig{ + ClientId: config.GoogleIDPTemplate.ClientID, + Scopes: config.GoogleIDPTemplate.Scopes, + }, + } + } if config.LDAPIDPTemplate != nil { providerConfig.Config = &idp_pb.ProviderConfig_Ldap{ Ldap: &idp_pb.LDAPConfig{ diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 8fb798a0b..0af131d54 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) 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 { + return nil, err + } + return &mgmt_pb.AddGoogleProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) UpdateGoogleProvider(ctx context.Context, req *mgmt_pb.UpdateGoogleProviderRequest) (*mgmt_pb.UpdateGoogleProviderResponse, error) { + details, err := s.command.UpdateOrgGoogleProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateGoogleProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateGoogleProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) AddLDAPProvider(ctx context.Context, req *mgmt_pb.AddLDAPProviderRequest) (*mgmt_pb.AddLDAPProviderResponse, error) { id, details, err := s.command.AddOrgLDAPProvider(ctx, authz.GetCtxData(ctx).OrgID, addLDAPProviderToCommand(req)) if err != nil { diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index ba65ef33d..792f87684 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -218,6 +218,26 @@ func providerQueryToQuery(idpQuery *mgmt_pb.ProviderQuery) (query.SearchQuery, e } } +func addGoogleProviderToCommand(req *mgmt_pb.AddGoogleProviderRequest) command.GoogleProvider { + return command.GoogleProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateGoogleProviderToCommand(req *mgmt_pb.UpdateGoogleProviderRequest) command.GoogleProvider { + return command.GoogleProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + func addLDAPProviderToCommand(req *mgmt_pb.AddLDAPProviderRequest) command.LDAPProvider { return command.LDAPProvider{ Name: req.Name, diff --git a/internal/command/idp.go b/internal/command/idp.go index 113bd3725..60f33ff5e 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -2,6 +2,14 @@ package command import "github.com/zitadel/zitadel/internal/repository/idp" +type GoogleProvider struct { + Name string + ClientID string + ClientSecret string + Scopes []string + IDPOptions idp.Options +} + type LDAPProvider struct { Name string Host string diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index f21e45e6c..1431f1412 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -1,6 +1,8 @@ package command import ( + "reflect" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -8,6 +10,93 @@ import ( "github.com/zitadel/zitadel/internal/repository/idpconfig" ) +type GoogleIDPWriteModel struct { + eventstore.WriteModel + + ID string + Name string + ClientID string + ClientSecret *crypto.CryptoValue + Scopes []string + idp.Options + + State domain.IDPState +} + +func (wm *GoogleIDPWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *idp.GoogleIDPAddedEvent: + wm.reduceAddedEvent(e) + case *idp.GoogleIDPChangedEvent: + wm.reduceChangedEvent(e) + case *idp.RemovedEvent: + wm.State = domain.IDPStateRemoved + } + } + return wm.WriteModel.Reduce() +} + +func (wm *GoogleIDPWriteModel) reduceAddedEvent(e *idp.GoogleIDPAddedEvent) { + wm.Name = e.Name + wm.ClientID = e.ClientID + wm.ClientSecret = e.ClientSecret + wm.Scopes = e.Scopes + wm.Options = e.Options + wm.State = domain.IDPStateActive +} + +func (wm *GoogleIDPWriteModel) reduceChangedEvent(e *idp.GoogleIDPChangedEvent) { + if e.Name != nil { + wm.Name = *e.Name + } + if e.ClientID != nil { + wm.ClientID = *e.ClientID + } + if e.ClientSecret != nil { + wm.ClientSecret = e.ClientSecret + } + if e.Scopes != nil { + wm.Scopes = e.Scopes + } + wm.Options.ReduceChanges(e.OptionChanges) +} + +func (wm *GoogleIDPWriteModel) NewChanges( + name string, + clientID string, + clientSecretString string, + secretCrypto crypto.Crypto, + scopes []string, + options idp.Options, +) ([]idp.GoogleIDPChanges, error) { + changes := make([]idp.GoogleIDPChanges, 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.ChangeGoogleClientSecret(clientSecret)) + } + if wm.Name != name { + changes = append(changes, idp.ChangeGoogleName(name)) + } + if wm.ClientID != clientID { + changes = append(changes, idp.ChangeGoogleClientID(clientID)) + } + if !reflect.DeepEqual(wm.Scopes, scopes) { + changes = append(changes, idp.ChangeGoogleScopes(scopes)) + } + + opts := wm.Options.Changes(options) + if !opts.IsZero() { + changes = append(changes, idp.ChangeGoogleOptions(opts)) + } + return changes, nil +} + type LDAPIDPWriteModel struct { eventstore.WriteModel @@ -34,7 +123,7 @@ func (wm *LDAPIDPWriteModel) Reduce() error { if wm.ID != e.ID { continue } - wm.reduceAddeddEvent(e) + wm.reduceAddedEvent(e) case *idp.LDAPIDPChangedEvent: if wm.ID != e.ID { continue @@ -50,7 +139,7 @@ func (wm *LDAPIDPWriteModel) Reduce() error { return wm.WriteModel.Reduce() } -func (wm *LDAPIDPWriteModel) reduceAddeddEvent(e *idp.LDAPIDPAddedEvent) { +func (wm *LDAPIDPWriteModel) reduceAddedEvent(e *idp.LDAPIDPAddedEvent) { wm.Name = e.Name wm.Host = e.Host wm.Port = e.Port @@ -170,6 +259,10 @@ type IDPRemoveWriteModel struct { func (wm *IDPRemoveWriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { + case *idp.GoogleIDPAddedEvent: + wm.reduceAdded(e.ID, e.Name) + case *idp.GoogleIDPChangedEvent: + wm.reduceChanged(e.ID, e.Name) case *idp.LDAPIDPAddedEvent: wm.reduceAdded(e.ID, e.Name) case *idp.LDAPIDPChangedEvent: diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 60f696ce5..e970c5033 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -13,6 +13,40 @@ 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()) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceGoogleProvider(instanceAgg, id, 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) 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)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{}, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) AddInstanceLDAPProvider(ctx context.Context, provider LDAPProvider) (string, *domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) id, err := c.idGenerator.Next() @@ -60,6 +94,87 @@ 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 { + 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") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + 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 + } + 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.NewGoogleIDPAddedEvent( + ctx, + &a.Aggregate, + id, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateInstanceGoogleProvider(a *instance.Aggregate, id string, provider GoogleProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if id = strings.TrimSpace(id); 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 + } + 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, + id, + provider.Name, + provider.ClientID, + provider.ClientSecret, + c.idpConfigEncryption, + 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) prepareAddInstanceLDAPProvider(a *instance.Aggregate, id string, provider LDAPProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index 6108ef256..d9757c323 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -9,6 +9,80 @@ import ( "github.com/zitadel/zitadel/internal/repository/instance" ) +type InstanceGoogleIDPWriteModel struct { + GoogleIDPWriteModel +} + +func NewGoogleInstanceIDPWriteModel(instanceID, id string) *InstanceGoogleIDPWriteModel { + return &InstanceGoogleIDPWriteModel{ + GoogleIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + }, + ID: id, + }, + } +} + +func (wm *InstanceGoogleIDPWriteModel) Reduce() error { + return wm.GoogleIDPWriteModel.Reduce() +} + +func (wm *InstanceGoogleIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *instance.GoogleIDPAddedEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) + case *instance.GoogleIDPChangedEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPChangedEvent) + case *instance.IDPRemovedEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.RemovedEvent) + } + } +} + +func (wm *InstanceGoogleIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.GoogleIDPAddedEventType, + instance.GoogleIDPChangedEventType, + instance.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *InstanceGoogleIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID, + clientSecretString string, + secretCrypto crypto.Crypto, + scopes []string, + options idp.Options, +) (*instance.GoogleIDPChangedEvent, error) { + + changes, err := wm.GoogleIDPWriteModel.NewChanges(name, clientID, clientSecretString, secretCrypto, scopes, options) + if err != nil { + return nil, err + } + if len(changes) == 0 { + return nil, nil + } + changeEvent, err := instance.NewGoogleIDPChangedEvent(ctx, aggregate, id, changes) + if err != nil { + return nil, err + } + return changeEvent, nil +} + type InstanceLDAPIDPWriteModel struct { LDAPIDPWriteModel } @@ -136,6 +210,10 @@ func (wm *InstanceIDPRemoveWriteModel) Reduce() error { func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { + case *instance.GoogleIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) + case *instance.GoogleIDPChangedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPChangedEvent) case *instance.LDAPIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) case *instance.LDAPIDPChangedEvent: @@ -155,6 +233,8 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { AggregateTypes(instance.AggregateType). AggregateIDs(wm.AggregateID). EventTypes( + instance.GoogleIDPAddedEventType, + instance.GoogleIDPChangedEventType, instance.LDAPIDPAddedEventType, instance.LDAPIDPChangedEventType, instance.IDPRemovedEventType, diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 2eb526950..d1279e113 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -20,6 +20,363 @@ import ( "github.com/zitadel/zitadel/internal/repository/instance" ) +func TestCommandSide_AddInstanceGoogleIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + provider GoogleProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GoogleProvider{}, + }, + 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: GoogleProvider{ + ClientID: "clientID", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewGoogleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + }, + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + 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.NewGoogleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + 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: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + 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.AddInstanceGoogleProvider(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_UpdateInstanceGoogleIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + id string + provider GoogleProvider + } + 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: GoogleProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GoogleProvider{}, + }, + 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: GoogleProvider{ + ClientID: "clientID", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewGoogleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + }, + }, + res: res{ + want: &domain.ObjectDetails{}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewGoogleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + func() eventstore.Command { + t := true + event, _ := instance.NewGoogleIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + []idp.GoogleIDPChanges{ + idp.ChangeGoogleClientID("clientID2"), + idp.ChangeGoogleClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + idp.ChangeGoogleScopes([]string{"openid", "profile"}), + idp.ChangeGoogleOptions(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: GoogleProvider{ + ClientID: "clientID2", + ClientSecret: "newSecret", + 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.UpdateInstanceGoogleProvider(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_AddInstanceLDAPIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index 2935b5731..97b95b33c 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -12,6 +12,40 @@ import ( "github.com/zitadel/zitadel/internal/repository/org" ) +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)) + 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) 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)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{}, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) AddOrgLDAPProvider(ctx context.Context, resourceOwner string, provider LDAPProvider) (string, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) id, err := c.idGenerator.Next() @@ -59,6 +93,78 @@ 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 { + 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) { + writeModel := NewGoogleOrgIDPWriteModel(resourceOwner, id) + 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, id, provider.Name, provider.ClientID, secret, provider.Scopes, provider.IDPOptions), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateOrgGoogleProvider(a *org.Aggregate, resourceOwner, id string, provider GoogleProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if id = strings.TrimSpace(id); 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 + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "ORG-Dqrg1", "Errors.Org.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + id, + provider.Name, + provider.ClientID, + provider.ClientSecret, + c.idpConfigEncryption, + 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) prepareAddOrgLDAPProvider(a *org.Aggregate, resourceOwner, id string, provider LDAPProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index 4ae982de1..e9b0d5077 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -9,6 +9,82 @@ import ( "github.com/zitadel/zitadel/internal/repository/org" ) +type OrgGoogleIDPWriteModel struct { + GoogleIDPWriteModel +} + +func NewGoogleOrgIDPWriteModel(orgID, id string) *OrgGoogleIDPWriteModel { + return &OrgGoogleIDPWriteModel{ + GoogleIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + ID: id, + }, + } +} + +func (wm *OrgGoogleIDPWriteModel) Reduce() error { + return wm.GoogleIDPWriteModel.Reduce() +} + +func (wm *OrgGoogleIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *org.GoogleIDPAddedEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) + case *org.GoogleIDPChangedEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPChangedEvent) + case *org.IDPRemovedEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.RemovedEvent) + default: + wm.GoogleIDPWriteModel.AppendEvents(e) + } + } +} + +func (wm *OrgGoogleIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + org.GoogleIDPAddedEventType, + org.GoogleIDPChangedEventType, + org.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *OrgGoogleIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID, + clientSecretString string, + secretCrypto crypto.Crypto, + scopes []string, + options idp.Options, +) (*org.GoogleIDPChangedEvent, error) { + + changes, err := wm.GoogleIDPWriteModel.NewChanges(name, clientID, clientSecretString, secretCrypto, scopes, options) + if err != nil { + return nil, err + } + if len(changes) == 0 { + return nil, nil + } + changeEvent, err := org.NewGoogleIDPChangedEvent(ctx, aggregate, id, changes) + if err != nil { + return nil, err + } + return changeEvent, nil +} + type OrgLDAPIDPWriteModel struct { LDAPIDPWriteModel } @@ -136,6 +212,10 @@ func (wm *OrgIDPRemoveWriteModel) Reduce() error { func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { + case *org.GoogleIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) + case *org.GoogleIDPChangedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPChangedEvent) case *org.LDAPIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) case *org.LDAPIDPChangedEvent: @@ -155,6 +235,8 @@ func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { AggregateTypes(org.AggregateType). AggregateIDs(wm.AggregateID). EventTypes( + org.GoogleIDPAddedEventType, + org.GoogleIDPChangedEventType, org.LDAPIDPAddedEventType, org.LDAPIDPChangedEventType, org.IDPRemovedEventType, diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index e8e1bfc88..b3bc1c4a7 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -18,6 +18,365 @@ import ( "github.com/zitadel/zitadel/internal/repository/org" ) +func TestCommandSide_AddOrgGoogleIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + provider GoogleProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GoogleProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientSecret", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GoogleProvider{ + ClientID: "clientID", + }, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + eventPusherToEvents( + org.NewGoogleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "ok all set", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + eventPusherToEvents( + org.NewGoogleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + 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: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + 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.AddOrgGoogleProvider(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_UpdateOrgGoogleIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + id string + provider GoogleProvider + } + 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: GoogleProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{}, + }, + res{ + err: caos_errors.IsErrorInvalidArgument, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewGoogleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + }, + }, + res: res{ + want: &domain.ObjectDetails{}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewGoogleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + func() eventstore.Command { + t := true + event, _ := org.NewGoogleIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.GoogleIDPChanges{ + idp.ChangeGoogleClientID("clientID2"), + idp.ChangeGoogleClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("newSecret"), + }), + idp.ChangeGoogleScopes([]string{"openid", "profile"}), + idp.ChangeGoogleOptions(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: GoogleProvider{ + ClientID: "clientID2", + ClientSecret: "newSecret", + 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.UpdateOrgGoogleProvider(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_AddOrgLDAPIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 5340ae7ab..d99b8a8bd 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query/projection" @@ -32,6 +33,7 @@ type IDPTemplate struct { IsLinkingAllowed bool IsAutoCreation bool IsAutoUpdate bool + *GoogleIDPTemplate *LDAPIDPTemplate } @@ -40,6 +42,13 @@ type IDPTemplates struct { Templates []*IDPTemplate } +type GoogleIDPTemplate struct { + IDPID string + ClientID string + ClientSecret *crypto.CryptoValue + Scopes database.StringArray +} + type LDAPIDPTemplate struct { IDPID string Host string @@ -51,7 +60,6 @@ type LDAPIDPTemplate struct { Admin string Password *crypto.CryptoValue idp.LDAPAttributes - idp.Options } var ( @@ -121,6 +129,33 @@ var ( } ) +var ( + googleIdpTemplateTable = table{ + name: projection.IDPTemplateGoogleTable, + instanceIDCol: projection.GoogleInstanceIDCol, + } + GoogleIDCol = Column{ + name: projection.GoogleIDCol, + table: googleIdpTemplateTable, + } + GoogleInstanceIDCol = Column{ + name: projection.GoogleInstanceIDCol, + table: googleIdpTemplateTable, + } + GoogleClientIDCol = Column{ + name: projection.GoogleClientIDCol, + table: googleIdpTemplateTable, + } + GoogleClientSecretCol = Column{ + name: projection.GoogleClientSecretCol, + table: googleIdpTemplateTable, + } + GoogleScopesCol = Column{ + name: projection.GoogleScopesCol, + table: googleIdpTemplateTable, + } +) + var ( ldapIdpTemplateTable = table{ name: projection.IDPTemplateLDAPTable, @@ -335,6 +370,10 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla IDPTemplateIsLinkingAllowedCol.identifier(), IDPTemplateIsAutoCreationCol.identifier(), IDPTemplateIsAutoUpdateCol.identifier(), + GoogleIDCol.identifier(), + GoogleClientIDCol.identifier(), + GoogleClientSecretCol.identifier(), + GoogleScopesCol.identifier(), LDAPIDCol.identifier(), LDAPHostCol.identifier(), LDAPPortCol.identifier(), @@ -358,11 +397,19 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla LDAPAvatarURLAttributeCol.identifier(), LDAPProfileAttributeCol.identifier(), ).From(idpTemplateTable.identifier()). + LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IDPTemplate, error) { idpTemplate := new(IDPTemplate) + name := sql.NullString{} + + googleID := sql.NullString{} + googleClientID := sql.NullString{} + googleClientSecret := new(crypto.CryptoValue) + googleScopes := database.StringArray{} + ldapID := sql.NullString{} ldapHost := sql.NullString{} ldapPort := sql.NullString{} @@ -393,13 +440,17 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla &idpTemplate.ChangeDate, &idpTemplate.Sequence, &idpTemplate.State, - &idpTemplate.Name, + &name, &idpTemplate.Type, &idpTemplate.OwnerType, &idpTemplate.IsCreationAllowed, &idpTemplate.IsLinkingAllowed, &idpTemplate.IsAutoCreation, &idpTemplate.IsAutoUpdate, + &googleID, + &googleClientID, + &googleClientSecret, + &googleScopes, &ldapID, &ldapHost, &ldapPort, @@ -430,7 +481,16 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla return nil, errors.ThrowInternal(err, "QUERY-ADG42", "Errors.Internal") } - if ldapID.Valid { + idpTemplate.Name = name.String + + if googleID.Valid { + idpTemplate.GoogleIDPTemplate = &GoogleIDPTemplate{ + IDPID: googleID.String, + ClientID: googleClientID.String, + ClientSecret: googleClientSecret, + Scopes: googleScopes, + } + } else if ldapID.Valid { idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{ IDPID: ldapID.String, Host: ldapHost.String, @@ -478,6 +538,10 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate IDPTemplateIsLinkingAllowedCol.identifier(), IDPTemplateIsAutoCreationCol.identifier(), IDPTemplateIsAutoUpdateCol.identifier(), + GoogleIDCol.identifier(), + GoogleClientIDCol.identifier(), + GoogleClientSecretCol.identifier(), + GoogleScopesCol.identifier(), LDAPIDCol.identifier(), LDAPHostCol.identifier(), LDAPPortCol.identifier(), @@ -502,6 +566,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate LDAPProfileAttributeCol.identifier(), countColumn.identifier(), ).From(idpTemplateTable.identifier()). + LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPTemplates, error) { @@ -510,6 +575,13 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate for rows.Next() { idpTemplate := new(IDPTemplate) + name := sql.NullString{} + + googleID := sql.NullString{} + googleClientID := sql.NullString{} + googleClientSecret := new(crypto.CryptoValue) + googleScopes := database.StringArray{} + ldapID := sql.NullString{} ldapHost := sql.NullString{} ldapPort := sql.NullString{} @@ -540,13 +612,17 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate &idpTemplate.ChangeDate, &idpTemplate.Sequence, &idpTemplate.State, - &idpTemplate.Name, + &name, &idpTemplate.Type, &idpTemplate.OwnerType, &idpTemplate.IsCreationAllowed, &idpTemplate.IsLinkingAllowed, &idpTemplate.IsAutoCreation, &idpTemplate.IsAutoUpdate, + &googleID, + &googleClientID, + &googleClientSecret, + &googleScopes, &ldapID, &ldapHost, &ldapPort, @@ -576,7 +652,16 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate return nil, err } - if ldapID.Valid { + idpTemplate.Name = name.String + + if googleID.Valid { + idpTemplate.GoogleIDPTemplate = &GoogleIDPTemplate{ + IDPID: googleID.String, + ClientID: googleClientID.String, + ClientSecret: googleClientSecret, + Scopes: googleScopes, + } + } else 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 d164fbcbe..e1a4167c1 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -8,6 +8,7 @@ import ( "regexp" "testing" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/repository/idp" @@ -27,6 +28,10 @@ var ( ` projections.idp_templates.is_linking_allowed,` + ` projections.idp_templates.is_auto_creation,` + ` projections.idp_templates.is_auto_update,` + + ` projections.idp_templates_google.idp_id,` + + ` projections.idp_templates_google.client_id,` + + ` projections.idp_templates_google.client_secret,` + + ` projections.idp_templates_google.scopes,` + ` projections.idp_templates_ldap.idp_id,` + ` projections.idp_templates_ldap.host,` + ` projections.idp_templates_ldap.port,` + @@ -50,6 +55,7 @@ var ( ` projections.idp_templates_ldap.avatar_url_attribute,` + ` projections.idp_templates_ldap.profile_attribute` + ` FROM projections.idp_templates` + + ` 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{ "id", @@ -65,6 +71,11 @@ var ( "is_linking_allowed", "is_auto_creation", "is_auto_update", + // google config + "idp_id", + "client_id", + "client_secret", + "scopes", // ldap config "idp_id", "host", @@ -102,6 +113,10 @@ var ( ` projections.idp_templates.is_linking_allowed,` + ` projections.idp_templates.is_auto_creation,` + ` projections.idp_templates.is_auto_update,` + + ` projections.idp_templates_google.idp_id,` + + ` projections.idp_templates_google.client_id,` + + ` projections.idp_templates_google.client_secret,` + + ` projections.idp_templates_google.scopes,` + ` projections.idp_templates_ldap.idp_id,` + ` projections.idp_templates_ldap.host,` + ` projections.idp_templates_ldap.port,` + @@ -126,6 +141,7 @@ var ( ` projections.idp_templates_ldap.profile_attribute,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_templates` + + ` 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{ "id", @@ -141,6 +157,12 @@ var ( "is_linking_allowed", "is_auto_creation", "is_auto_update", + // google config + "idp_id", + "client_id", + "client_secret", + "scopes", + // ldap config "idp_id", "host", "port", @@ -196,6 +218,81 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { }, object: (*IDPTemplate)(nil), }, + + { + name: "prepareIDPTemplateByIDQuery google 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.IDPTypeGoogle, + domain.IdentityProviderTypeOrg, + true, + true, + true, + true, + // google + "idp-id", + "client_id", + nil, + database.StringArray{"profile"}, + // 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.IDPTypeGoogle, + OwnerType: domain.IdentityProviderTypeOrg, + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + GoogleIDPTemplate: &GoogleIDPTemplate{ + IDPID: "idp-id", + ClientID: "client_id", + ClientSecret: nil, + Scopes: []string{"profile"}, + }, + }, + }, { name: "prepareIDPTemplateByIDQuery ldap idp", prepare: prepareIDPTemplateByIDQuery, @@ -217,6 +314,11 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // google + nil, + nil, + nil, + nil, // ldap config "idp-id", "host", @@ -305,6 +407,11 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // google config + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -404,6 +511,11 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // google config + nil, + nil, + nil, + nil, // ldap config "idp-id", "host", @@ -501,6 +613,11 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // google config + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -560,7 +677,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { idpTemplatesCols, [][]driver.Value{ { - "idp-id-1", + "idp-id-ldap", "ro", testNow, testNow, @@ -573,8 +690,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { true, true, true, + // google config + nil, + nil, + nil, + nil, // ldap config - "idp-id", + "idp-id-ldap", "host", "port", true, @@ -598,19 +720,24 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "profile", }, { - "idp-id-2", + "idp-id-google", "ro", testNow, testNow, uint64(20211109), domain.IDPConfigStateActive, "idp-name", - domain.IDPTypeLDAP, + domain.IDPTypeGoogle, domain.IdentityProviderTypeOrg, true, true, true, true, + // google + "idp-id-google", + "client_id", + nil, + database.StringArray{"profile"}, // ldap config nil, nil, @@ -648,7 +775,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211109, ResourceOwner: "ro", - ID: "idp-id-1", + ID: "idp-id-ldap", State: domain.IDPStateActive, Name: "idp-name", Type: domain.IDPTypeLDAP, @@ -658,7 +785,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { IsAutoCreation: true, IsAutoUpdate: true, LDAPIDPTemplate: &LDAPIDPTemplate{ - IDPID: "idp-id", + IDPID: "idp-id-ldap", Host: "host", Port: "port", TLS: true, @@ -688,15 +815,21 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { ChangeDate: testNow, Sequence: 20211109, ResourceOwner: "ro", - ID: "idp-id-2", + ID: "idp-id-google", State: domain.IDPStateActive, Name: "idp-name", - Type: domain.IDPTypeLDAP, + Type: domain.IDPTypeGoogle, OwnerType: domain.IdentityProviderTypeOrg, IsCreationAllowed: true, IsLinkingAllowed: true, IsAutoCreation: true, IsAutoUpdate: true, + GoogleIDPTemplate: &GoogleIDPTemplate{ + IDPID: "idp-id-google", + ClientID: "client_id", + ClientSecret: nil, + Scopes: []string{"profile"}, + }, }, }, }, diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index fa25113a2..1c7c3750a 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -2,7 +2,9 @@ package projection import ( "context" + "time" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -14,10 +16,12 @@ import ( ) const ( - IDPTemplateTable = "projections.idp_templates" - IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix + IDPTemplateTable = "projections.idp_templates" + IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix + IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix - IDPTemplateLDAPSuffix = "ldap" + IDPTemplateGoogleSuffix = "google" + IDPTemplateLDAPSuffix = "ldap" IDPTemplateIDCol = "id" IDPTemplateCreationDateCol = "creation_date" @@ -35,6 +39,12 @@ const ( IDPTemplateIsAutoCreationCol = "is_auto_creation" IDPTemplateIsAutoUpdateCol = "is_auto_update" + GoogleIDCol = "idp_id" + GoogleInstanceIDCol = "instance_id" + GoogleClientIDCol = "client_id" + GoogleClientSecretCol = "client_secret" + GoogleScopesCol = "scopes" + LDAPIDCol = "idp_id" LDAPInstanceIDCol = "instance_id" LDAPHostCol = "host" @@ -90,6 +100,17 @@ 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(GoogleIDCol, crdb.ColumnTypeText), + crdb.NewColumn(GoogleInstanceIDCol, crdb.ColumnTypeText), + crdb.NewColumn(GoogleClientIDCol, crdb.ColumnTypeText), + crdb.NewColumn(GoogleClientSecretCol, crdb.ColumnTypeJSONB), + crdb.NewColumn(GoogleScopesCol, crdb.ColumnTypeTextArray, crdb.Nullable()), + }, + crdb.NewPrimaryKey(GoogleInstanceIDCol, GoogleIDCol), + IDPTemplateGoogleSuffix, + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()), + ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(LDAPIDCol, crdb.ColumnTypeText), crdb.NewColumn(LDAPInstanceIDCol, crdb.ColumnTypeText), @@ -129,6 +150,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { { Aggregate: instance.AggregateType, EventRedusers: []handler.EventReducer{ + { + Event: instance.GoogleIDPAddedEventType, + Reduce: p.reduceGoogleIDPAdded, + }, + { + Event: instance.GoogleIDPChangedEventType, + Reduce: p.reduceGoogleIDPChanged, + }, { Event: instance.LDAPIDPAddedEventType, Reduce: p.reduceLDAPIDPAdded, @@ -150,6 +179,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { { Aggregate: org.AggregateType, EventRedusers: []handler.EventReducer{ + { + Event: org.GoogleIDPAddedEventType, + Reduce: p.reduceGoogleIDPAdded, + }, + { + Event: org.GoogleIDPChangedEventType, + Reduce: p.reduceGoogleIDPChanged, + }, { Event: org.LDAPIDPAddedEventType, Reduce: p.reduceLDAPIDPAdded, @@ -171,6 +208,94 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { } } +func (p *idpTemplateProjection) reduceGoogleIDPAdded(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.GoogleIDPAddedEvent + var idpOwnerType domain.IdentityProviderType + switch e := event.(type) { + case *org.GoogleIDPAddedEvent: + idpEvent = e.GoogleIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeOrg + case *instance.GoogleIDPAddedEvent: + idpEvent = e.GoogleIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeSystem + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ap9ihb", "reduce.wrong.event.type %v", []eventstore.EventType{org.GoogleIDPAddedEventType, instance.GoogleIDPAddedEventType}) + } + + 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.IDPTypeGoogle), + 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(GoogleIDCol, idpEvent.ID), + handler.NewCol(GoogleInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(GoogleClientIDCol, idpEvent.ClientID), + handler.NewCol(GoogleClientSecretCol, idpEvent.ClientSecret), + handler.NewCol(GoogleScopesCol, database.StringArray(idpEvent.Scopes)), + }, + crdb.WithTableSuffix(IDPTemplateGoogleSuffix), + ), + ), nil +} + +func (p *idpTemplateProjection) reduceGoogleIDPChanged(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.GoogleIDPChangedEvent + switch e := event.(type) { + case *org.GoogleIDPChangedEvent: + idpEvent = e.GoogleIDPChangedEvent + case *instance.GoogleIDPChangedEvent: + idpEvent = e.GoogleIDPChangedEvent + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.GoogleIDPChangedEventType, instance.GoogleIDPChangedEventType}) + } + + 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), + }, + ), + ) + googleCols := reduceGoogleIDPChangedColumns(idpEvent) + if len(googleCols) > 0 { + ops = append(ops, + crdb.AddUpdateStatement( + googleCols, + []handler.Condition{ + handler.NewCond(GoogleIDCol, idpEvent.ID), + handler.NewCond(GoogleInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(IDPTemplateGoogleSuffix), + ), + ) + } + + return crdb.NewMultiStatement( + &idpEvent, + ops..., + ), nil +} + func (p *idpTemplateProjection) reduceLDAPIDPAdded(event eventstore.Event) (*handler.Statement, error) { var idpEvent idp.LDAPIDPAddedEvent var idpOwnerType domain.IdentityProviderType @@ -247,13 +372,10 @@ func (p *idpTemplateProjection) reduceLDAPIDPChanged(event eventstore.Event) (*h return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.LDAPIDPChangedEventType, instance.LDAPIDPChangedEventType}) } - cols := reduceLDAPIDPChangedTemplateColumns(idpEvent) - ldapCols := reduceLDAPIDPChangedLDAPColumns(idpEvent) - ops := make([]func(eventstore.Event) crdb.Exec, 0, 2) ops = append(ops, crdb.AddUpdateStatement( - cols, + reduceIDPChangedTemplateColumns(idpEvent.Name, idpEvent.CreationDate(), idpEvent.Sequence(), idpEvent.OptionChanges), []handler.Condition{ handler.NewCond(IDPTemplateIDCol, idpEvent.ID), handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), @@ -261,6 +383,7 @@ func (p *idpTemplateProjection) reduceLDAPIDPChanged(event eventstore.Event) (*h ), ) + ldapCols := reduceLDAPIDPChangedColumns(idpEvent) if len(ldapCols) > 0 { ops = append(ops, crdb.AddUpdateStatement( @@ -320,30 +443,44 @@ func (p *idpTemplateProjection) reduceOwnerRemoved(event eventstore.Event) (*han ), nil } -func reduceLDAPIDPChangedTemplateColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column { +func reduceIDPChangedTemplateColumns(name *string, creationDate time.Time, sequence uint64, optionChanges idp.OptionChanges) []handler.Column { cols := make([]handler.Column, 0, 7) - if idpEvent.Name != nil { - cols = append(cols, handler.NewCol(IDPTemplateNameCol, *idpEvent.Name)) + if name != nil { + cols = append(cols, handler.NewCol(IDPTemplateNameCol, *name)) } - if idpEvent.IsCreationAllowed != nil { - cols = append(cols, handler.NewCol(IDPTemplateIsCreationAllowedCol, *idpEvent.IsCreationAllowed)) + if optionChanges.IsCreationAllowed != nil { + cols = append(cols, handler.NewCol(IDPTemplateIsCreationAllowedCol, *optionChanges.IsCreationAllowed)) } - if idpEvent.IsLinkingAllowed != nil { - cols = append(cols, handler.NewCol(IDPTemplateIsLinkingAllowedCol, *idpEvent.IsLinkingAllowed)) + if optionChanges.IsLinkingAllowed != nil { + cols = append(cols, handler.NewCol(IDPTemplateIsLinkingAllowedCol, *optionChanges.IsLinkingAllowed)) } - if idpEvent.IsAutoCreation != nil { - cols = append(cols, handler.NewCol(IDPTemplateIsAutoCreationCol, *idpEvent.IsAutoCreation)) + if optionChanges.IsAutoCreation != nil { + cols = append(cols, handler.NewCol(IDPTemplateIsAutoCreationCol, *optionChanges.IsAutoCreation)) } - if idpEvent.IsAutoUpdate != nil { - cols = append(cols, handler.NewCol(IDPTemplateIsAutoUpdateCol, *idpEvent.IsAutoUpdate)) + if optionChanges.IsAutoUpdate != nil { + cols = append(cols, handler.NewCol(IDPTemplateIsAutoUpdateCol, *optionChanges.IsAutoUpdate)) } return append(cols, - handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), - handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateChangeDateCol, creationDate), + handler.NewCol(IDPTemplateSequenceCol, sequence), ) } -func reduceLDAPIDPChangedLDAPColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column { +func reduceGoogleIDPChangedColumns(idpEvent idp.GoogleIDPChangedEvent) []handler.Column { + googleCols := make([]handler.Column, 0, 3) + if idpEvent.ClientID != nil { + googleCols = append(googleCols, handler.NewCol(GoogleClientIDCol, *idpEvent.ClientID)) + } + if idpEvent.ClientSecret != nil { + googleCols = append(googleCols, handler.NewCol(GoogleClientSecretCol, *idpEvent.ClientSecret)) + } + if idpEvent.Scopes != nil { + googleCols = append(googleCols, handler.NewCol(GoogleScopesCol, database.StringArray(idpEvent.Scopes))) + } + return googleCols +} + +func reduceLDAPIDPChangedColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column { ldapCols := make([]handler.Column, 0, 4) if idpEvent.Host != nil { ldapCols = append(ldapCols, handler.NewCol(LDAPHostCol, *idpEvent.Host)) diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index ed67f0ec2..7de4bf9a2 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -3,6 +3,7 @@ package projection import ( "testing" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -124,6 +125,254 @@ func TestIDPTemplateProjection_reducesRemove(t *testing.T) { } } +func TestIDPTemplateProjection_reducesGoogle(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 reduceGoogleIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.GoogleIDPAddedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "clientID": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.GoogleIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceGoogleIDPAdded, + 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, + "", + domain.IdentityProviderTypeSystem, + domain.IDPTypeGoogle, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates_google (idp_id, instance_id, client_id, client_secret, scopes) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + }, + }, + }, + }, + }, + }, + { + name: "org reduceGoogleIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.GoogleIDPAddedEventType), + org.AggregateType, + []byte(`{ + "id": "idp-id", + "clientID": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), org.GoogleIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceGoogleIDPAdded, + 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, + "", + domain.IdentityProviderTypeOrg, + domain.IDPTypeGoogle, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates_google (idp_id, instance_id, client_id, client_secret, scopes) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + }, + }, + }, + }, + }, + }, + { + name: "instance reduceGoogleIDPChanged minimal", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.GoogleIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "isCreationAllowed": true, + "clientID": "id" +}`), + ), instance.GoogleIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceGoogleIDPChanged, + 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_google SET client_id = $1 WHERE (idp_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "id", + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceGoogleIDPChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.GoogleIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "clientID": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.GoogleIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceGoogleIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates SET (is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update, change_date, sequence) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", + expectedArgs: []interface{}{ + true, + true, + true, + true, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates_google SET (client_id, client_secret, scopes) = ($1, $2, $3) WHERE (idp_id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + "client_id", + anyArg{}, + 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_reducesLDAP(t *testing.T) { type args struct { event func(t *testing.T) eventstore.Event diff --git a/internal/repository/idp/google.go b/internal/repository/idp/google.go new file mode 100644 index 000000000..5cff7eb33 --- /dev/null +++ b/internal/repository/idp/google.go @@ -0,0 +1,143 @@ +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 GoogleIDPAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name string `json:"name,omitempty"` + ClientID string `json:"clientID"` + ClientSecret *crypto.CryptoValue `json:"clientSecret"` + Scopes []string `json:"scopes,omitempty"` + Options +} + +func NewGoogleIDPAddedEvent( + base *eventstore.BaseEvent, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options Options, +) *GoogleIDPAddedEvent { + return &GoogleIDPAddedEvent{ + BaseEvent: *base, + ID: id, + Name: name, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopes, + Options: options, + } +} + +func (e *GoogleIDPAddedEvent) Data() interface{} { + return e +} + +func (e *GoogleIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func GoogleIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &GoogleIDPAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-SAff1", "unable to unmarshal event") + } + + return e, nil +} + +type GoogleIDPChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name *string `json:"name,omitempty"` + ClientID *string `json:"clientID,omitempty"` + ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` + Scopes []string `json:"scopes,omitempty"` + OptionChanges +} + +func NewGoogleIDPChangedEvent( + base *eventstore.BaseEvent, + id string, + changes []GoogleIDPChanges, +) (*GoogleIDPChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "IDP-Dg3qs", "Errors.NoChangesFound") + } + changedEvent := &GoogleIDPChangedEvent{ + BaseEvent: *base, + ID: id, + } + for _, change := range changes { + change(changedEvent) + } + return changedEvent, nil +} + +type GoogleIDPChanges func(*GoogleIDPChangedEvent) + +func ChangeGoogleName(name string) func(*GoogleIDPChangedEvent) { + return func(e *GoogleIDPChangedEvent) { + e.Name = &name + } +} +func ChangeGoogleClientID(clientID string) func(*GoogleIDPChangedEvent) { + return func(e *GoogleIDPChangedEvent) { + e.ClientID = &clientID + } +} + +func ChangeGoogleClientSecret(clientSecret *crypto.CryptoValue) func(*GoogleIDPChangedEvent) { + return func(e *GoogleIDPChangedEvent) { + e.ClientSecret = clientSecret + } +} + +func ChangeGoogleScopes(scopes []string) func(*GoogleIDPChangedEvent) { + return func(e *GoogleIDPChangedEvent) { + e.Scopes = scopes + } +} + +func ChangeGoogleOptions(options OptionChanges) func(*GoogleIDPChangedEvent) { + return func(e *GoogleIDPChangedEvent) { + e.OptionChanges = options + } +} + +func (e *GoogleIDPChangedEvent) Data() interface{} { + return e +} + +func (e *GoogleIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func GoogleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &GoogleIDPChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-SF3t2", "unable to unmarshal event") + } + + return e, nil +} diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 642044619..18fd6cff1 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, GoogleIDPAddedEventType, GoogleIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper). diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index a0ae9580a..cb580cbe6 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -10,11 +10,89 @@ import ( ) const ( - LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.added" - LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.changed" - IDPRemovedEventType eventstore.EventType = "instance.idp.removed" + GoogleIDPAddedEventType eventstore.EventType = "instance.idp.google.added" + GoogleIDPChangedEventType eventstore.EventType = "instance.idp.google.changed" + LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.added" + LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.changed" + IDPRemovedEventType eventstore.EventType = "instance.idp.removed" ) +type GoogleIDPAddedEvent struct { + idp.GoogleIDPAddedEvent +} + +func NewGoogleIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options idp.Options, +) *GoogleIDPAddedEvent { + + return &GoogleIDPAddedEvent{ + GoogleIDPAddedEvent: *idp.NewGoogleIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + GoogleIDPAddedEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + options, + ), + } +} + +func GoogleIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.GoogleIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &GoogleIDPAddedEvent{GoogleIDPAddedEvent: *e.(*idp.GoogleIDPAddedEvent)}, nil +} + +type GoogleIDPChangedEvent struct { + idp.GoogleIDPChangedEvent +} + +func NewGoogleIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.GoogleIDPChanges, +) (*GoogleIDPChangedEvent, error) { + + changedEvent, err := idp.NewGoogleIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + GoogleIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &GoogleIDPChangedEvent{GoogleIDPChangedEvent: *changedEvent}, nil +} + +func GoogleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.GoogleIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &GoogleIDPChangedEvent{GoogleIDPChangedEvent: *e.(*idp.GoogleIDPChangedEvent)}, nil +} + type LDAPIDPAddedEvent struct { idp.LDAPIDPAddedEvent } diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 4e8d556cc..f3d7f86d5 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, GoogleIDPAddedEventType, GoogleIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper). diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index fe3b971d7..c41e8537d 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -10,11 +10,89 @@ import ( ) const ( - LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added" - LDAPIDPChangedEventType eventstore.EventType = "org.idp.ldap.changed" - IDPRemovedEventType eventstore.EventType = "org.idp.removed" + GoogleIDPAddedEventType eventstore.EventType = "org.idp.google.added" + GoogleIDPChangedEventType eventstore.EventType = "org.idp.google.changed" + LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added" + LDAPIDPChangedEventType eventstore.EventType = "org.idp.ldap.changed" + IDPRemovedEventType eventstore.EventType = "org.idp.removed" ) +type GoogleIDPAddedEvent struct { + idp.GoogleIDPAddedEvent +} + +func NewGoogleIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options idp.Options, +) *GoogleIDPAddedEvent { + + return &GoogleIDPAddedEvent{ + GoogleIDPAddedEvent: *idp.NewGoogleIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + GoogleIDPAddedEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + options, + ), + } +} + +func GoogleIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.GoogleIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &GoogleIDPAddedEvent{GoogleIDPAddedEvent: *e.(*idp.GoogleIDPAddedEvent)}, nil +} + +type GoogleIDPChangedEvent struct { + idp.GoogleIDPChangedEvent +} + +func NewGoogleIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.GoogleIDPChanges, +) (*GoogleIDPChangedEvent, error) { + + changedEvent, err := idp.NewGoogleIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + GoogleIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &GoogleIDPChangedEvent{GoogleIDPChangedEvent: *changedEvent}, nil +} + +func GoogleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.GoogleIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &GoogleIDPChangedEvent{GoogleIDPChangedEvent: *e.(*idp.GoogleIDPChangedEvent)}, nil +} + type LDAPIDPAddedEvent struct { idp.LDAPIDPAddedEvent } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index f66bfab81..70754ee4f 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1033,7 +1033,31 @@ service AdminService { }; } - // Add a new ldap identity provider on the instance + // Add a new Google identity provider on the instance + rpc AddGoogleProvider(AddGoogleProviderRequest) returns (AddGoogleProviderResponse) { + option (google.api.http) = { + post: "/idps/google" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + } + + // Change an existing Google identity provider on the instance + rpc UpdateGoogleProvider(UpdateGoogleProviderRequest) returns (UpdateGoogleProviderResponse) { + option (google.api.http) = { + put: "/idps/google/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + } + + // Add a new LDAP identity provider on the instance rpc AddLDAPProvider(AddLDAPProviderRequest) returns (AddLDAPProviderResponse) { option (google.api.http) = { post: "/idps/ldap" @@ -1045,7 +1069,7 @@ service AdminService { }; } - // Change an existing ldap identity provider on the instance + // Change an existing LDAP identity provider on the instance rpc UpdateLDAPProvider(UpdateLDAPProviderRequest) returns (UpdateLDAPProviderResponse) { option (google.api.http) = { put: "/idps/ldap/{id}" @@ -3710,6 +3734,34 @@ message GetProviderByIDResponse { zitadel.idp.v1.Provider idp = 1; } +message AddGoogleProviderRequest { + // Google will be used as default, if no name is provided + string name = 1 [(validate.rules).string = {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}]; + repeated string scopes = 4 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 5; +} + +message AddGoogleProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateGoogleProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {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}]; + repeated string scopes = 5 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 6; +} + +message UpdateGoogleProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddLDAPProviderRequest { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string host = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 94a3e622d..eae33cf02 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -262,9 +262,15 @@ message ProviderConfig { Options options = 1; oneof config { LDAPConfig ldap = 2; + GoogleConfig google = 3; } } +message GoogleConfig { + string client_id = 1; + repeated string scopes = 2; +} + message LDAPConfig { string host = 1; string port = 2; diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index b9d0e0a61..534945ec3 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -3031,7 +3031,32 @@ service ManagementService { }; } - // Add a new ldap identity provider in the organisation + + // Add a new Google identity provider in the organisation + rpc AddGoogleProvider(AddGoogleProviderRequest) returns (AddGoogleProviderResponse) { + option (google.api.http) = { + post: "/idps/google" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + } + + // Change an existing Google identity provider in the organisation + rpc UpdateGoogleProvider(UpdateGoogleProviderRequest) returns (UpdateGoogleProviderResponse) { + option (google.api.http) = { + put: "/idps/google/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + } + + // Add a new LDAP identity provider in the organisation rpc AddLDAPProvider(AddLDAPProviderRequest) returns (AddLDAPProviderResponse) { option (google.api.http) = { post: "/idps/ldap" @@ -3043,7 +3068,7 @@ service ManagementService { }; } - // Change an existing ldap identity provider in the organisation + // Change an existing LDAP identity provider in the organisation rpc UpdateLDAPProvider(UpdateLDAPProviderRequest) returns (UpdateLDAPProviderResponse) { option (google.api.http) = { put: "/idps/ldap/{id}" @@ -6008,6 +6033,34 @@ message GetProviderByIDResponse { zitadel.idp.v1.Provider idp = 1; } +message AddGoogleProviderRequest { + // Google will be used as default, if no name is provided + string name = 1 [(validate.rules).string = {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}]; + repeated string scopes = 4 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 5; +} + +message AddGoogleProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateGoogleProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {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}]; + repeated string scopes = 5 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 6; +} + +message UpdateGoogleProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddLDAPProviderRequest { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string host = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];