diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index dab331b793..a66a6e0684 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -241,6 +241,27 @@ func (s *Server) UpdateJWTProvider(ctx context.Context, req *admin_pb.UpdateJWTP }, nil } +func (s *Server) AddAzureADProvider(ctx context.Context, req *admin_pb.AddAzureADProviderRequest) (*admin_pb.AddAzureADProviderResponse, error) { + id, details, err := s.command.AddInstanceAzureADProvider(ctx, addAzureADProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.AddAzureADProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) UpdateAzureADProvider(ctx context.Context, req *admin_pb.UpdateAzureADProviderRequest) (*admin_pb.UpdateAzureADProviderResponse, error) { + details, err := s.command.UpdateInstanceAzureADProvider(ctx, req.Id, updateAzureADProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateAzureADProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) AddGitHubProvider(ctx context.Context, req *admin_pb.AddGitHubProviderRequest) (*admin_pb.AddGitHubProviderResponse, error) { id, details, err := s.command.AddInstanceGitHubProvider(ctx, addGitHubProviderToCommand(req)) if err != nil { diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 9a46f21069..0cb4ae499d 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -273,6 +273,30 @@ func updateJWTProviderToCommand(req *admin_pb.UpdateJWTProviderRequest) command. } } +func addAzureADProviderToCommand(req *admin_pb.AddAzureADProviderRequest) command.AzureADProvider { + return command.AzureADProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant), + EmailVerified: req.EmailVerified, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateAzureADProviderToCommand(req *admin_pb.UpdateAzureADProviderRequest) command.AzureADProvider { + return command.AzureADProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant), + EmailVerified: req.EmailVerified, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + func addGitHubProviderToCommand(req *admin_pb.AddGitHubProviderRequest) command.GitHubProvider { return command.GitHubProvider{ Name: req.Name, diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index 5dbd53c553..a831555b65 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -4,6 +4,7 @@ import ( obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/domain" iam_model "github.com/zitadel/zitadel/internal/iam/model" + "github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/idp" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" @@ -329,6 +330,33 @@ func LDAPAttributesToCommand(attributes *idp_pb.LDAPAttributes) idp.LDAPAttribut } } +func AzureADTenantToCommand(tenant *idp_pb.AzureADTenant) string { + if tenant == nil { + return string(azuread.CommonTenant) + } + switch t := tenant.Type.(type) { + case *idp_pb.AzureADTenant_TenantType: + return string(azureADTenantTypeToCommand(t.TenantType)) + case *idp_pb.AzureADTenant_TenantId: + return t.TenantId + default: + return string(azuread.CommonTenant) + } +} + +func azureADTenantTypeToCommand(tenantType idp_pb.AzureADTenantType) azuread.TenantType { + switch tenantType { + case idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_COMMON: + return azuread.CommonTenant + case idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_ORGANISATIONS: + return azuread.OrganizationsTenant + case idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_CONSUMERS: + return azuread.ConsumersTenant + default: + return azuread.CommonTenant + } +} + func ProvidersToPb(providers []*query.IDPTemplate) []*idp_pb.Provider { list := make([]*idp_pb.Provider, len(providers)) for i, provider := range providers { @@ -412,6 +440,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig { jwtConfigToPb(providerConfig, config.JWTIDPTemplate) return providerConfig } + if config.AzureADIDPTemplate != nil { + azureConfigToPb(providerConfig, config.AzureADIDPTemplate) + return providerConfig + } if config.GitHubIDPTemplate != nil { githubConfigToPb(providerConfig, config.GitHubIDPTemplate) return providerConfig @@ -473,6 +505,32 @@ func jwtConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.JWTIDP } } +func azureConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.AzureADIDPTemplate) { + providerConfig.Config = &idp_pb.ProviderConfig_AzureAd{ + AzureAd: &idp_pb.AzureADConfig{ + ClientId: template.ClientID, + Tenant: azureTenantToPb(template.Tenant), + EmailVerified: template.IsEmailVerified, + Scopes: template.Scopes, + }, + } +} + +func azureTenantToPb(tenant string) *idp_pb.AzureADTenant { + var tenantType idp_pb.IsAzureADTenantType + switch azuread.TenantType(tenant) { + case azuread.CommonTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_COMMON} + case azuread.OrganizationsTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_ORGANISATIONS} + case azuread.ConsumersTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_CONSUMERS} + default: + tenantType = &idp_pb.AzureADTenant_TenantId{TenantId: tenant} + } + return &idp_pb.AzureADTenant{Type: tenantType} +} + func githubConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.GitHubIDPTemplate) { providerConfig.Config = &idp_pb.ProviderConfig_Github{ Github: &idp_pb.GitHubConfig{ diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 62e0e9a91b..66c7b4c6a7 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -233,6 +233,27 @@ func (s *Server) UpdateJWTProvider(ctx context.Context, req *mgmt_pb.UpdateJWTPr }, nil } +func (s *Server) AddAzureADProvider(ctx context.Context, req *mgmt_pb.AddAzureADProviderRequest) (*mgmt_pb.AddAzureADProviderResponse, error) { + id, details, err := s.command.AddOrgAzureADProvider(ctx, authz.GetCtxData(ctx).OrgID, addAzureADProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.AddAzureADProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) UpdateAzureADProvider(ctx context.Context, req *mgmt_pb.UpdateAzureADProviderRequest) (*mgmt_pb.UpdateAzureADProviderResponse, error) { + details, err := s.command.UpdateOrgAzureADProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateAzureADProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateAzureADProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) AddGitHubProvider(ctx context.Context, req *mgmt_pb.AddGitHubProviderRequest) (*mgmt_pb.AddGitHubProviderResponse, error) { id, details, err := s.command.AddOrgGitHubProvider(ctx, authz.GetCtxData(ctx).OrgID, addGitHubProviderToCommand(req)) if err != nil { diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index f2466c8a9b..ad78492668 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -290,6 +290,28 @@ func updateJWTProviderToCommand(req *mgmt_pb.UpdateJWTProviderRequest) command.J } } +func addAzureADProviderToCommand(req *mgmt_pb.AddAzureADProviderRequest) command.AzureADProvider { + return command.AzureADProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant), + EmailVerified: req.EmailVerified, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateAzureADProviderToCommand(req *mgmt_pb.UpdateAzureADProviderRequest) command.AzureADProvider { + return command.AzureADProvider{ + Name: req.Name, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant), + EmailVerified: req.EmailVerified, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + func addGitHubProviderToCommand(req *mgmt_pb.AddGitHubProviderRequest) command.GitHubProvider { return command.GitHubProvider{ Name: req.Name, diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 711c6572e8..7e5edab1dd 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -18,6 +18,7 @@ import ( "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/github" "github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/google" @@ -144,6 +145,8 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai provider, err = l.oidcProvider(r.Context(), identityProvider) case domain.IDPTypeJWT: provider, err = l.jwtProvider(identityProvider) + case domain.IDPTypeAzureAD: + provider, err = l.azureProvider(r.Context(), identityProvider) case domain.IDPTypeGitHub: provider, err = l.githubProvider(r.Context(), identityProvider) case domain.IDPTypeGitHubEnterprise: @@ -155,7 +158,6 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai case domain.IDPTypeGoogle: provider, err = l.googleProvider(r.Context(), identityProvider) case domain.IDPTypeLDAP, - domain.IDPTypeAzureAD, domain.IDPTypeUnspecified: fallthrough default: @@ -212,6 +214,13 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque return } session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code} + case domain.IDPTypeAzureAD: + provider, err = l.azureProvider(r.Context(), identityProvider) + if err != nil { + l.externalAuthFailed(w, r, authReq, nil, nil, err) + return + } + session = &oauth.Session{Provider: provider.(*azuread.Provider).Provider, Code: data.Code} case domain.IDPTypeGitHub: provider, err = l.githubProvider(r.Context(), identityProvider) if err != nil { @@ -249,7 +258,6 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code} case domain.IDPTypeJWT, domain.IDPTypeLDAP, - domain.IDPTypeAzureAD, domain.IDPTypeUnspecified: fallthrough default: @@ -666,6 +674,28 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe ) } +func (l *Login) azureProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*azuread.Provider, error) { + secret, err := crypto.DecryptString(identityProvider.AzureADIDPTemplate.ClientSecret, l.idpConfigAlg) + if err != nil { + return nil, err + } + opts := make([]azuread.ProviderOptions, 0, 2) + if identityProvider.AzureADIDPTemplate.IsEmailVerified { + opts = append(opts, azuread.WithEmailVerified()) + } + if identityProvider.AzureADIDPTemplate.Tenant != "" { + opts = append(opts, azuread.WithTenant(azuread.TenantType(identityProvider.AzureADIDPTemplate.Tenant))) + } + return azuread.New( + identityProvider.Name, + identityProvider.AzureADIDPTemplate.ClientID, + secret, + l.baseURL(ctx)+EndpointExternalLoginCallback, + identityProvider.AzureADIDPTemplate.Scopes, + opts..., + ) +} + func (l *Login) githubProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*github.Provider, error) { secret, err := crypto.DecryptString(identityProvider.GitHubIDPTemplate.ClientSecret, l.idpConfigAlg) if err != nil { diff --git a/internal/command/idp.go b/internal/command/idp.go index 660d0cc7c3..6e4030b646 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -38,6 +38,16 @@ type JWTProvider struct { IDPOptions idp.Options } +type AzureADProvider struct { + Name string + ClientID string + ClientSecret string + Scopes []string + Tenant string + EmailVerified bool + IDPOptions idp.Options +} + type GitHubProvider struct { Name string ClientID string diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 147df0291a..498955ddd5 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -413,6 +413,111 @@ func (wm *JWTIDPWriteModel) reduceJWTConfigChangedEvent(e *idpconfig.JWTConfigCh } } +type AzureADIDPWriteModel struct { + eventstore.WriteModel + + ID string + Name string + ClientID string + ClientSecret *crypto.CryptoValue + Scopes []string + Tenant string + IsEmailVerified bool + idp.Options + + State domain.IDPState +} + +func (wm *AzureADIDPWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *idp.AzureADIDPAddedEvent: + wm.reduceAddedEvent(e) + case *idp.AzureADIDPChangedEvent: + wm.reduceChangedEvent(e) + case *idp.RemovedEvent: + wm.State = domain.IDPStateRemoved + } + } + return wm.WriteModel.Reduce() +} + +func (wm *AzureADIDPWriteModel) reduceAddedEvent(e *idp.AzureADIDPAddedEvent) { + wm.Name = e.Name + wm.ClientID = e.ClientID + wm.ClientSecret = e.ClientSecret + wm.Scopes = e.Scopes + wm.Tenant = e.Tenant + wm.IsEmailVerified = e.IsEmailVerified + wm.Options = e.Options + wm.State = domain.IDPStateActive +} + +func (wm *AzureADIDPWriteModel) reduceChangedEvent(e *idp.AzureADIDPChangedEvent) { + if e.ClientID != nil { + wm.ClientID = *e.ClientID + } + if e.ClientSecret != nil { + wm.ClientSecret = e.ClientSecret + } + if e.Name != nil { + wm.Name = *e.Name + } + if e.Scopes != nil { + wm.Scopes = e.Scopes + } + if e.Tenant != nil { + wm.Tenant = *e.Tenant + } + if e.IsEmailVerified != nil { + wm.IsEmailVerified = *e.IsEmailVerified + } + wm.Options.ReduceChanges(e.OptionChanges) +} + +func (wm *AzureADIDPWriteModel) NewChanges( + name string, + clientID string, + clientSecretString string, + secretCrypto crypto.Crypto, + scopes []string, + tenant string, + isEmailVerified bool, + options idp.Options, +) ([]idp.AzureADIDPChanges, error) { + changes := make([]idp.AzureADIDPChanges, 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.ChangeAzureADClientSecret(clientSecret)) + } + if wm.Name != name { + changes = append(changes, idp.ChangeAzureADName(name)) + } + if wm.ClientID != clientID { + changes = append(changes, idp.ChangeAzureADClientID(clientID)) + } + if wm.Tenant != tenant { + changes = append(changes, idp.ChangeAzureADTenant(tenant)) + } + if wm.IsEmailVerified != isEmailVerified { + changes = append(changes, idp.ChangeAzureADIsEmailVerified(isEmailVerified)) + } + if !reflect.DeepEqual(wm.Scopes, scopes) { + changes = append(changes, idp.ChangeAzureADScopes(scopes)) + } + + opts := wm.Options.Changes(options) + if !opts.IsZero() { + changes = append(changes, idp.ChangeAzureADOptions(opts)) + } + return changes, nil +} + type GitHubIDPWriteModel struct { eventstore.WriteModel @@ -1049,6 +1154,8 @@ func (wm *IDPRemoveWriteModel) Reduce() error { wm.reduceAdded(e.ID) case *idp.JWTIDPAddedEvent: wm.reduceAdded(e.ID) + case *idp.AzureADIDPAddedEvent: + wm.reduceAdded(e.ID) case *idp.GitHubIDPAddedEvent: wm.reduceAdded(e.ID) case *idp.GitHubEnterpriseIDPAddedEvent: diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 422ca44215..75e1e5c328 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -139,6 +139,48 @@ func (c *Commands) UpdateInstanceJWTProvider(ctx context.Context, id string, pro return pushedEventsToObjectDetails(pushedEvents), nil } +func (c *Commands) AddInstanceAzureADProvider(ctx context.Context, provider AzureADProvider) (string, *domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + writeModel := NewAzureADInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceAzureADProvider(instanceAgg, writeModel, provider)) + if err != nil { + return "", nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return "", nil, err + } + return id, pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) UpdateInstanceAzureADProvider(ctx context.Context, id string, provider AzureADProvider) (*domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + writeModel := NewAzureADInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceAzureADProvider(instanceAgg, writeModel, provider)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) AddInstanceGitHubProvider(ctx context.Context, provider GitHubProvider) (string, *domain.ObjectDetails, error) { instanceID := authz.GetInstance(ctx).InstanceID() instanceAgg := instance.NewAggregate(instanceID) @@ -719,6 +761,92 @@ func (c *Commands) prepareUpdateInstanceJWTProvider(a *instance.Aggregate, write } } +func (c *Commands) prepareAddInstanceAzureADProvider(a *instance.Aggregate, writeModel *InstanceAzureADIDPWriteModel, provider AzureADProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdf3g", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fhbr2", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dzh3g", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + instance.NewAzureADIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.Tenant, + provider.EmailVerified, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateInstanceAzureADProvider(a *instance.Aggregate, writeModel *InstanceAzureADIDPWriteModel, provider AzureADProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAgh2", "Errors.Invalid.Argument") + } + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-fh3h1", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-dmitg", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INST-BHz3q", "Errors.Instance.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + provider.ClientSecret, + c.idpConfigEncryption, + provider.Scopes, + provider.Tenant, + provider.EmailVerified, + provider.IDPOptions, + ) + if err != nil || event == nil { + return nil, err + } + return []eventstore.Command{event}, nil + }, nil + } +} + func (c *Commands) prepareAddInstanceGitHubProvider(a *instance.Aggregate, writeModel *InstanceGitHubIDPWriteModel, provider GitHubProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index d8eb93eb83..f11f9e8e28 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -279,6 +279,82 @@ func (wm *InstanceJWTIDPWriteModel) NewChangedEvent( return instance.NewJWTIDPChangedEvent(ctx, aggregate, id, changes) } +type InstanceAzureADIDPWriteModel struct { + AzureADIDPWriteModel +} + +func NewAzureADInstanceIDPWriteModel(instanceID, id string) *InstanceAzureADIDPWriteModel { + return &InstanceAzureADIDPWriteModel{ + AzureADIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + }, + ID: id, + }, + } +} + +func (wm *InstanceAzureADIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *instance.AzureADIDPAddedEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPAddedEvent) + case *instance.AzureADIDPChangedEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPChangedEvent) + case *instance.IDPRemovedEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.RemovedEvent) + default: + wm.AzureADIDPWriteModel.AppendEvents(e) + } + } +} + +func (wm *InstanceAzureADIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.AzureADIDPAddedEventType, + instance.AzureADIDPChangedEventType, + instance.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *InstanceAzureADIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID, + clientSecretString string, + secretCrypto crypto.Crypto, + scopes []string, + tenant string, + isEmailVerified bool, + options idp.Options, +) (*instance.AzureADIDPChangedEvent, error) { + + changes, err := wm.AzureADIDPWriteModel.NewChanges( + name, + clientID, + clientSecretString, + secretCrypto, + scopes, + tenant, + isEmailVerified, + options, + ) + if err != nil || len(changes) == 0 { + return nil, err + } + return instance.NewAzureADIDPChangedEvent(ctx, aggregate, id, changes) +} + type InstanceGitHubIDPWriteModel struct { GitHubIDPWriteModel } @@ -726,6 +802,8 @@ func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) wm.IDPRemoveWriteModel.AppendEvents(&e.OIDCIDPAddedEvent) case *instance.JWTIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.JWTIDPAddedEvent) + case *instance.AzureADIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.AzureADIDPAddedEvent) case *instance.GitHubIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.GitHubIDPAddedEvent) case *instance.GitHubEnterpriseIDPAddedEvent: @@ -760,6 +838,7 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { instance.OAuthIDPAddedEventType, instance.OIDCIDPAddedEventType, instance.JWTIDPAddedEventType, + instance.AzureADIDPAddedEventType, instance.GitHubIDPAddedEventType, instance.GitHubEnterpriseIDPAddedEventType, instance.GitLabIDPAddedEventType, diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index e609a80b33..a79fd8fadc 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -637,6 +637,428 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) { } } +func TestCommandSide_AddInstanceAzureADIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + provider AzureADProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-sdf3g", "")) + }, + }, + }, + { + "invalid client id", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: AzureADProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Fhbr2", "")) + }, + }, + }, + { + "invalid client secret", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Dzh3g", "")) + }, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewAzureADIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + )), + }, + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: AzureADProvider{ + Name: "name", + 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.NewAzureADIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + "tenant", + true, + 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: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + Tenant: "tenant", + EmailVerified: true, + 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.AddInstanceAzureADProvider(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_UpdateInstanceAzureADIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + id string + provider AzureADProvider + } + 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: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SAgh2", "")) + }, + }, + }, + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-fh3h1", "")) + }, + }, + }, + { + "invalid client id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: AzureADProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-dmitg", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewAzureADIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewAzureADIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + func() eventstore.Command { + t := true + event, _ := instance.NewAzureADIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + []idp.AzureADIDPChanges{ + idp.ChangeAzureADName("new name"), + idp.ChangeAzureADClientID("new clientID"), + idp.ChangeAzureADClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("new clientSecret"), + }), + idp.ChangeAzureADScopes([]string{"openid", "profile"}), + idp.ChangeAzureADTenant("new tenant"), + idp.ChangeAzureADIsEmailVerified(true), + idp.ChangeAzureADOptions(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: AzureADProvider{ + Name: "new name", + ClientID: "new clientID", + ClientSecret: "new clientSecret", + Scopes: []string{"openid", "profile"}, + Tenant: "new tenant", + EmailVerified: true, + 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.UpdateInstanceAzureADProvider(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_AddInstanceGitHubIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index ca7ad66d27..9a47c9e571 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -131,6 +131,45 @@ func (c *Commands) UpdateOrgJWTProvider(ctx context.Context, resourceOwner, id s } return pushedEventsToObjectDetails(pushedEvents), nil } +func (c *Commands) AddOrgAzureADProvider(ctx context.Context, resourceOwner string, provider AzureADProvider) (string, *domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + writeModel := NewAzureADOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgAzureADProvider(orgAgg, writeModel, provider)) + if err != nil { + return "", nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return "", nil, err + } + return id, pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) UpdateOrgAzureADProvider(ctx context.Context, resourceOwner, id string, provider AzureADProvider) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + writeModel := NewAzureADOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgAzureADProvider(orgAgg, writeModel, provider)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} func (c *Commands) AddOrgGitHubProvider(ctx context.Context, resourceOwner string, provider GitHubProvider) (string, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) @@ -700,6 +739,92 @@ func (c *Commands) prepareUpdateOrgJWTProvider(a *org.Aggregate, writeModel *Org } } +func (c *Commands) prepareAddOrgAzureADProvider(a *org.Aggregate, writeModel *OrgAzureADIDPWriteModel, provider AzureADProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdf3g", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fhbr2", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dzh3g", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + org.NewAzureADIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.Tenant, + provider.EmailVerified, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateOrgAzureADProvider(a *org.Aggregate, writeModel *OrgAzureADIDPWriteModel, provider AzureADProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAgh2", "Errors.Invalid.Argument") + } + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-fh3h1", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-dmitg", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "ORG-BHz3q", "Errors.Org.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + provider.ClientSecret, + c.idpConfigEncryption, + provider.Scopes, + provider.Tenant, + provider.EmailVerified, + provider.IDPOptions, + ) + if err != nil || event == nil { + return nil, err + } + return []eventstore.Command{event}, nil + }, nil + } +} + func (c *Commands) prepareAddOrgGitHubProvider(a *org.Aggregate, writeModel *OrgGitHubIDPWriteModel, provider GitHubProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index c8335885b2..10438ecd62 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -281,6 +281,86 @@ func (wm *OrgJWTIDPWriteModel) NewChangedEvent( return org.NewJWTIDPChangedEvent(ctx, aggregate, id, changes) } +type OrgAzureADIDPWriteModel struct { + AzureADIDPWriteModel +} + +func NewAzureADOrgIDPWriteModel(orgID, id string) *OrgAzureADIDPWriteModel { + return &OrgAzureADIDPWriteModel{ + AzureADIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + ID: id, + }, + } +} + +func (wm *OrgAzureADIDPWriteModel) Reduce() error { + return wm.AzureADIDPWriteModel.Reduce() +} + +func (wm *OrgAzureADIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *org.AzureADIDPAddedEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPAddedEvent) + case *org.AzureADIDPChangedEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPChangedEvent) + case *org.IDPRemovedEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.RemovedEvent) + default: + wm.AzureADIDPWriteModel.AppendEvents(e) + } + } +} + +func (wm *OrgAzureADIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + org.AzureADIDPAddedEventType, + org.AzureADIDPChangedEventType, + org.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *OrgAzureADIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID, + clientSecretString string, + secretCrypto crypto.Crypto, + scopes []string, + tenant string, + isEmailVerified bool, + options idp.Options, +) (*org.AzureADIDPChangedEvent, error) { + + changes, err := wm.AzureADIDPWriteModel.NewChanges( + name, + clientID, + clientSecretString, + secretCrypto, + scopes, + tenant, + isEmailVerified, + options, + ) + if err != nil || len(changes) == 0 { + return nil, err + } + return org.NewAzureADIDPChangedEvent(ctx, aggregate, id, changes) +} + type OrgGitHubIDPWriteModel struct { GitHubIDPWriteModel } @@ -732,6 +812,8 @@ func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) { wm.IDPRemoveWriteModel.AppendEvents(&e.OIDCIDPAddedEvent) case *org.JWTIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.JWTIDPAddedEvent) + case *org.AzureADIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.AzureADIDPAddedEvent) case *org.GitHubIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.GitHubIDPAddedEvent) case *org.GitHubEnterpriseIDPAddedEvent: @@ -766,6 +848,7 @@ func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { org.OAuthIDPAddedEventType, org.OIDCIDPAddedEventType, org.JWTIDPAddedEventType, + org.AzureADIDPAddedEventType, org.GitHubIDPAddedEventType, org.GitHubEnterpriseIDPAddedEventType, org.GitLabIDPAddedEventType, diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index b15f8e515c..61568cb672 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -648,6 +648,432 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) { } } +func TestCommandSide_AddOrgAzureADIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + provider AzureADProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-sdf3g", "")) + }, + }, + }, + { + "invalid client id", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: AzureADProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Fhbr2", "")) + }, + }, + }, + { + "invalid client secret", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Dzh3g", "")) + }, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + eventPusherToEvents( + org.NewAzureADIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + )), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: AzureADProvider{ + Name: "name", + 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.NewAzureADIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + "tenant", + true, + 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: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + Tenant: "tenant", + EmailVerified: true, + 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.AddOrgAzureADProvider(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_UpdateOrgAzureADIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + id string + provider AzureADProvider + } + 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: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-SAgh2", "")) + }, + }, + }, + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-fh3h1", "")) + }, + }, + }, + { + "invalid client id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: AzureADProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-dmitg", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewAzureADIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewAzureADIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + func() eventstore.Command { + t := true + event, _ := org.NewAzureADIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.AzureADIDPChanges{ + idp.ChangeAzureADName("new name"), + idp.ChangeAzureADClientID("new clientID"), + idp.ChangeAzureADClientSecret(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("new clientSecret"), + }), + idp.ChangeAzureADScopes([]string{"openid", "profile"}), + idp.ChangeAzureADTenant("new tenant"), + idp.ChangeAzureADIsEmailVerified(true), + idp.ChangeAzureADOptions(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: AzureADProvider{ + Name: "new name", + ClientID: "new clientID", + ClientSecret: "new clientSecret", + Scopes: []string{"openid", "profile"}, + Tenant: "new tenant", + EmailVerified: true, + 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.UpdateOrgAzureADProvider(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_AddOrgGitHubIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/idp/providers/azuread/azuread.go b/internal/idp/providers/azuread/azuread.go index aede0ceff0..2d19b44aad 100644 --- a/internal/idp/providers/azuread/azuread.go +++ b/internal/idp/providers/azuread/azuread.go @@ -15,7 +15,7 @@ import ( const ( authURLTemplate string = "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize" tokenURLTemplate string = "https://login.microsoftonline.com/%s/oauth2/v2.0/token" - userinfoURL string = "https://graph.microsoft.com/oidc/userinfo" + userinfoURL string = "https://graph.microsoft.com/v1.0/me" ) // TenantType are the well known tenant types to scope the users that can authenticate. TenantType is not an @@ -73,7 +73,7 @@ func WithOAuthOptions(opts ...oauth.ProviderOpts) ProviderOptions { // New creates an AzureAD provider using the [oauth.Provider] (OAuth 2.0 generic provider). // By default, it uses the [CommonTenant] and unverified emails. -func New(name, clientID, clientSecret, redirectURI string, opts ...ProviderOptions) (*Provider, error) { +func New(name, clientID, clientSecret, redirectURI string, scopes []string, opts ...ProviderOptions) (*Provider, error) { provider := &Provider{ tenant: CommonTenant, options: make([]oauth.ProviderOpts, 0), @@ -81,7 +81,7 @@ func New(name, clientID, clientSecret, redirectURI string, opts ...ProviderOptio for _, opt := range opts { opt(provider) } - config := newConfig(provider.tenant, clientID, clientSecret, redirectURI, []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail}) + config := newConfig(provider.tenant, clientID, clientSecret, redirectURI, scopes) rp, err := oauth.New( config, name, @@ -121,34 +121,38 @@ func newConfig(tenant TenantType, clientID, secret, callbackURL string, scopes [ // AzureAD does not return an `email_verified` claim. // The verification can be automatically activated on the provider ([WithEmailVerified]) type User struct { - Sub string `json:"sub"` - FamilyName string `json:"family_name"` - GivenName string `json:"given_name"` - Name string `json:"name"` - PreferredUsername string `json:"preferred_username"` - Email domain.EmailAddress `json:"email"` - Picture string `json:"picture"` + ID string `json:"id"` + BusinessPhones []domain.PhoneNumber `json:"businessPhones"` + DisplayName string `json:"displayName"` + FirstName string `json:"givenName"` + JobTitle string `json:"jobTitle"` + Email domain.EmailAddress `json:"mail"` + MobilePhone domain.PhoneNumber `json:"mobilePhone"` + OfficeLocation string `json:"officeLocation"` + PreferredLanguage string `json:"preferredLanguage"` + LastName string `json:"surname"` + UserPrincipalName string `json:"userPrincipalName"` isEmailVerified bool } // GetID is an implementation of the [idp.User] interface. func (u *User) GetID() string { - return u.Sub + return u.ID } // GetFirstName is an implementation of the [idp.User] interface. func (u *User) GetFirstName() string { - return u.GivenName + return u.FirstName } // GetLastName is an implementation of the [idp.User] interface. func (u *User) GetLastName() string { - return u.FamilyName + return u.LastName } // GetDisplayName is an implementation of the [idp.User] interface. func (u *User) GetDisplayName() string { - return u.Name + return u.DisplayName } // GetNickname is an implementation of the [idp.User] interface. @@ -159,11 +163,16 @@ func (u *User) GetNickname() string { // GetPreferredUsername is an implementation of the [idp.User] interface. func (u *User) GetPreferredUsername() string { - return u.PreferredUsername + return u.UserPrincipalName } // GetEmail is an implementation of the [idp.User] interface. func (u *User) GetEmail() domain.EmailAddress { + if u.Email == "" { + // if the user used a social login on Azure as well, the email will be empty + // but is used as username + return domain.EmailAddress(u.UserPrincipalName) + } return u.Email } @@ -188,10 +197,8 @@ func (u *User) IsPhoneVerified() bool { } // GetPreferredLanguage is an implementation of the [idp.User] interface. -// It returns [language.Und] because AzureAD does not provide the user's language func (u *User) GetPreferredLanguage() language.Tag { - // AzureAD does not provide the user's language - return language.Und + return language.Make(u.PreferredLanguage) } // GetProfile is an implementation of the [idp.User] interface. @@ -202,5 +209,5 @@ func (u *User) GetProfile() string { // GetAvatarURL is an implementation of the [idp.User] interface. func (u *User) GetAvatarURL() string { - return u.Picture + return "" } diff --git a/internal/idp/providers/azuread/azuread_test.go b/internal/idp/providers/azuread/azuread_test.go index f257d9dc4a..16d72d0b43 100644 --- a/internal/idp/providers/azuread/azuread_test.go +++ b/internal/idp/providers/azuread/azuread_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v2/pkg/client/rp" + openid "github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oauth" @@ -19,6 +20,7 @@ func TestProvider_BeginAuth(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string options []ProviderOptions } tests := []struct { @@ -34,7 +36,7 @@ func TestProvider_BeginAuth(t *testing.T) { redirectURI: "redirectURI", }, want: &oidc.Session{ - AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid+profile+email&state=testState", + AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState", }, }, { @@ -48,7 +50,22 @@ func TestProvider_BeginAuth(t *testing.T) { }, }, want: &oidc.Session{ - AuthURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid+profile+email&state=testState", + AuthURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState", + }, + }, + { + name: "custom scopes", + fields: fields{ + clientID: "clientID", + clientSecret: "clientSecret", + redirectURI: "redirectURI", + scopes: []string{openid.ScopeOpenID, openid.ScopeProfile, "user"}, + options: []ProviderOptions{ + WithTenant(ConsumersTenant), + }, + }, + want: &oidc.Session{ + AuthURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid+profile+user&state=testState", }, }, } @@ -57,7 +74,7 @@ func TestProvider_BeginAuth(t *testing.T) { a := assert.New(t) r := require.New(t) - provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...) + provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...) r.NoError(err) session, err := provider.BeginAuth(context.Background(), "testState") @@ -74,6 +91,7 @@ func TestProvider_Options(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string options []ProviderOptions } type want struct { @@ -98,6 +116,7 @@ func TestProvider_Options(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: nil, options: nil, }, want: want{ @@ -146,7 +165,7 @@ func TestProvider_Options(t *testing.T) { t.Run(tt.name, func(t *testing.T) { a := assert.New(t) - provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...) + provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...) require.NoError(t, err) a.Equal(tt.want.name, provider.Name()) diff --git a/internal/idp/providers/azuread/session_test.go b/internal/idp/providers/azuread/session_test.go index d49a0b4f87..884841fa43 100644 --- a/internal/idp/providers/azuread/session_test.go +++ b/internal/idp/providers/azuread/session_test.go @@ -25,6 +25,7 @@ func TestSession_FetchUser(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string httpMock func() options []ProviderOptions authURL string @@ -61,7 +62,7 @@ func TestSession_FetchUser(t *testing.T) { redirectURI: "redirectURI", httpMock: func() { gock.New("https://graph.microsoft.com"). - Get("/oidc/userinfo"). + Get("/v1.0/me"). Reply(200). JSON(userinfo()) }, @@ -82,7 +83,7 @@ func TestSession_FetchUser(t *testing.T) { redirectURI: "redirectURI", httpMock: func() { gock.New("https://graph.microsoft.com"). - Get("/oidc/userinfo"). + Get("/v1.0/me"). Reply(http.StatusInternalServerError) }, authURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=openid+profile+email&state=testState", @@ -119,7 +120,7 @@ func TestSession_FetchUser(t *testing.T) { redirectURI: "redirectURI", httpMock: func() { gock.New("https://graph.microsoft.com"). - Get("/oidc/userinfo"). + Get("/v1.0/me"). Reply(200). JSON(userinfo()) }, @@ -145,16 +146,20 @@ func TestSession_FetchUser(t *testing.T) { }, want: want{ user: &User{ - Sub: "sub", - FamilyName: "lastname", - GivenName: "firstname", - Name: "firstname lastname", - PreferredUsername: "username", + ID: "id", + BusinessPhones: []domain.PhoneNumber{"phone1", "phone2"}, + DisplayName: "firstname lastname", + FirstName: "firstname", + JobTitle: "title", Email: "email", - Picture: "picture", + MobilePhone: "mobile", + OfficeLocation: "office", + PreferredLanguage: "en", + LastName: "lastname", + UserPrincipalName: "username", isEmailVerified: false, }, - id: "sub", + id: "id", firstName: "firstname", lastName: "lastname", displayName: "firstname lastname", @@ -164,8 +169,7 @@ func TestSession_FetchUser(t *testing.T) { isEmailVerified: false, phone: "", isPhoneVerified: false, - preferredLanguage: language.Und, - avatarURL: "picture", + preferredLanguage: language.English, profile: "", }, }, @@ -180,7 +184,7 @@ func TestSession_FetchUser(t *testing.T) { }, httpMock: func() { gock.New("https://graph.microsoft.com"). - Get("/oidc/userinfo"). + Get("/v1.0/me"). Reply(200). JSON(userinfo()) }, @@ -206,16 +210,20 @@ func TestSession_FetchUser(t *testing.T) { }, want: want{ user: &User{ - Sub: "sub", - FamilyName: "lastname", - GivenName: "firstname", - Name: "firstname lastname", - PreferredUsername: "username", + ID: "id", + BusinessPhones: []domain.PhoneNumber{"phone1", "phone2"}, + DisplayName: "firstname lastname", + FirstName: "firstname", + JobTitle: "title", Email: "email", - Picture: "picture", + MobilePhone: "mobile", + OfficeLocation: "office", + PreferredLanguage: "en", + LastName: "lastname", + UserPrincipalName: "username", isEmailVerified: true, }, - id: "sub", + id: "id", firstName: "firstname", lastName: "lastname", displayName: "firstname lastname", @@ -225,8 +233,7 @@ func TestSession_FetchUser(t *testing.T) { isEmailVerified: true, phone: "", isPhoneVerified: false, - preferredLanguage: language.Und, - avatarURL: "picture", + preferredLanguage: language.English, profile: "", }, }, @@ -237,7 +244,7 @@ func TestSession_FetchUser(t *testing.T) { tt.fields.httpMock() a := assert.New(t) - provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...) + provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...) require.NoError(t, err) session := &oauth.Session{ @@ -272,15 +279,18 @@ func TestSession_FetchUser(t *testing.T) { } } -func userinfo() oidc.UserInfoSetter { - userinfo := oidc.NewUserInfo() - userinfo.SetSubject("sub") - userinfo.SetName("firstname lastname") - userinfo.SetPreferredUsername("username") - userinfo.SetNickname("nickname") - userinfo.SetEmail("email", false) // azure add does not send the email_verified claim - userinfo.SetPicture("picture") - userinfo.SetGivenName("firstname") - userinfo.SetFamilyName("lastname") - return userinfo +func userinfo() *User { + return &User{ + ID: "id", + BusinessPhones: []domain.PhoneNumber{"phone1", "phone2"}, + DisplayName: "firstname lastname", + FirstName: "firstname", + JobTitle: "title", + Email: "email", + MobilePhone: "mobile", + OfficeLocation: "office", + PreferredLanguage: "en", + LastName: "lastname", + UserPrincipalName: "username", + } } diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 098cf4b749..a5e2b3b69e 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -37,6 +37,7 @@ type IDPTemplate struct { *OAuthIDPTemplate *OIDCIDPTemplate *JWTIDPTemplate + *AzureADIDPTemplate *GitHubIDPTemplate *GitHubEnterpriseIDPTemplate *GitLabIDPTemplate @@ -77,6 +78,15 @@ type JWTIDPTemplate struct { Endpoint string } +type AzureADIDPTemplate struct { + IDPID string + ClientID string + ClientSecret *crypto.CryptoValue + Scopes database.StringArray + Tenant string + IsEmailVerified bool +} + type GitHubIDPTemplate struct { IDPID string ClientID string @@ -301,6 +311,41 @@ var ( } ) +var ( + azureadIdpTemplateTable = table{ + name: projection.IDPTemplateAzureADTable, + instanceIDCol: projection.AzureADInstanceIDCol, + } + AzureADIDCol = Column{ + name: projection.AzureADIDCol, + table: azureadIdpTemplateTable, + } + AzureADInstanceIDCol = Column{ + name: projection.AzureADInstanceIDCol, + table: azureadIdpTemplateTable, + } + AzureADClientIDCol = Column{ + name: projection.AzureADClientIDCol, + table: azureadIdpTemplateTable, + } + AzureADClientSecretCol = Column{ + name: projection.AzureADClientSecretCol, + table: azureadIdpTemplateTable, + } + AzureADScopesCol = Column{ + name: projection.AzureADScopesCol, + table: azureadIdpTemplateTable, + } + AzureADTenantCol = Column{ + name: projection.AzureADTenantCol, + table: azureadIdpTemplateTable, + } + AzureADIsEmailVerified = Column{ + name: projection.AzureADIsEmailVerified, + table: azureadIdpTemplateTable, + } +) + var ( githubIdpTemplateTable = table{ name: projection.IDPTemplateGitHubTable, @@ -683,6 +728,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se JWTEndpointCol.identifier(), JWTKeysEndpointCol.identifier(), JWTHeaderNameCol.identifier(), + // azure + AzureADIDCol.identifier(), + AzureADClientIDCol.identifier(), + AzureADClientSecretCol.identifier(), + AzureADScopesCol.identifier(), + AzureADTenantCol.identifier(), + AzureADIsEmailVerified.identifier(), // github GitHubIDCol.identifier(), GitHubClientIDCol.identifier(), @@ -739,6 +791,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)). LeftJoin(join(OIDCIDCol, IDPTemplateIDCol)). LeftJoin(join(JWTIDCol, IDPTemplateIDCol)). + LeftJoin(join(AzureADIDCol, IDPTemplateIDCol)). LeftJoin(join(GitHubIDCol, IDPTemplateIDCol)). LeftJoin(join(GitHubEnterpriseIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)). @@ -772,6 +825,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se jwtKeysEndpoint := sql.NullString{} jwtHeaderName := sql.NullString{} + azureadID := sql.NullString{} + azureadClientID := sql.NullString{} + azureadClientSecret := new(crypto.CryptoValue) + azureadScopes := database.StringArray{} + azureadTenant := sql.NullString{} + azureadIsEmailVerified := sql.NullBool{} + githubID := sql.NullString{} githubClientID := sql.NullString{} githubClientSecret := new(crypto.CryptoValue) @@ -859,6 +919,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se &jwtEndpoint, &jwtKeysEndpoint, &jwtHeaderName, + // azure + &azureadID, + &azureadClientID, + &azureadClientSecret, + &azureadScopes, + &azureadTenant, + &azureadIsEmailVerified, // github &githubID, &githubClientID, @@ -951,6 +1018,16 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se Endpoint: jwtEndpoint.String, } } + if azureadID.Valid { + idpTemplate.AzureADIDPTemplate = &AzureADIDPTemplate{ + IDPID: azureadID.String, + ClientID: azureadClientID.String, + ClientSecret: azureadClientSecret, + Scopes: azureadScopes, + Tenant: azureadTenant.String, + IsEmailVerified: azureadIsEmailVerified.Bool, + } + } if githubID.Valid { idpTemplate.GitHubIDPTemplate = &GitHubIDPTemplate{ IDPID: githubID.String, @@ -1064,6 +1141,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec JWTEndpointCol.identifier(), JWTKeysEndpointCol.identifier(), JWTHeaderNameCol.identifier(), + // azure + AzureADIDCol.identifier(), + AzureADClientIDCol.identifier(), + AzureADClientSecretCol.identifier(), + AzureADScopesCol.identifier(), + AzureADTenantCol.identifier(), + AzureADIsEmailVerified.identifier(), // github GitHubIDCol.identifier(), GitHubClientIDCol.identifier(), @@ -1121,6 +1205,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)). LeftJoin(join(OIDCIDCol, IDPTemplateIDCol)). LeftJoin(join(JWTIDCol, IDPTemplateIDCol)). + LeftJoin(join(AzureADIDCol, IDPTemplateIDCol)). LeftJoin(join(GitHubIDCol, IDPTemplateIDCol)). LeftJoin(join(GitHubEnterpriseIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)). @@ -1157,6 +1242,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec jwtKeysEndpoint := sql.NullString{} jwtHeaderName := sql.NullString{} + azureadID := sql.NullString{} + azureadClientID := sql.NullString{} + azureadClientSecret := new(crypto.CryptoValue) + azureadScopes := database.StringArray{} + azureadTenant := sql.NullString{} + azureadIsEmailVerified := sql.NullBool{} + githubID := sql.NullString{} githubClientID := sql.NullString{} githubClientSecret := new(crypto.CryptoValue) @@ -1244,6 +1336,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec &jwtEndpoint, &jwtKeysEndpoint, &jwtHeaderName, + // azure + &azureadID, + &azureadClientID, + &azureadClientSecret, + &azureadScopes, + &azureadTenant, + &azureadIsEmailVerified, // github &githubID, &githubClientID, @@ -1335,6 +1434,16 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec Endpoint: jwtEndpoint.String, } } + if azureadID.Valid { + idpTemplate.AzureADIDPTemplate = &AzureADIDPTemplate{ + IDPID: azureadID.String, + ClientID: azureadClientID.String, + ClientSecret: azureadClientSecret, + Scopes: azureadScopes, + Tenant: azureadTenant.String, + IsEmailVerified: azureadIsEmailVerified.Bool, + } + } if githubID.Valid { idpTemplate.GitHubIDPTemplate = &GitHubIDPTemplate{ IDPID: githubID.String, diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index aab57dfdb4..d7b93293ba 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -49,6 +49,13 @@ var ( ` projections.idp_templates3_jwt.jwt_endpoint,` + ` projections.idp_templates3_jwt.keys_endpoint,` + ` projections.idp_templates3_jwt.header_name,` + + // azure + ` projections.idp_templates3_azure.idp_id,` + + ` projections.idp_templates3_azure.client_id,` + + ` projections.idp_templates3_azure.client_secret,` + + ` projections.idp_templates3_azure.scopes,` + + ` projections.idp_templates3_azure.tenant,` + + ` projections.idp_templates3_azure.is_email_verified,` + // github ` projections.idp_templates3_github.idp_id,` + ` projections.idp_templates3_github.client_id,` + @@ -105,6 +112,7 @@ var ( ` LEFT JOIN projections.idp_templates3_oauth2 ON projections.idp_templates3.id = projections.idp_templates3_oauth2.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_oauth2.instance_id` + ` LEFT JOIN projections.idp_templates3_oidc ON projections.idp_templates3.id = projections.idp_templates3_oidc.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_oidc.instance_id` + ` LEFT JOIN projections.idp_templates3_jwt ON projections.idp_templates3.id = projections.idp_templates3_jwt.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_jwt.instance_id` + + ` LEFT JOIN projections.idp_templates3_azure ON projections.idp_templates3.id = projections.idp_templates3_azure.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_azure.instance_id` + ` LEFT JOIN projections.idp_templates3_github ON projections.idp_templates3.id = projections.idp_templates3_github.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_github.instance_id` + ` LEFT JOIN projections.idp_templates3_github_enterprise ON projections.idp_templates3.id = projections.idp_templates3_github_enterprise.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_github_enterprise.instance_id` + ` LEFT JOIN projections.idp_templates3_gitlab ON projections.idp_templates3.id = projections.idp_templates3_gitlab.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_gitlab.instance_id` + @@ -147,6 +155,13 @@ var ( "jwt_endpoint", "keys_endpoint", "header_name", + // azure + "idp_id", + "client_id", + "client_secret", + "scopes", + "tenant", + "is_email_verified", // github config "idp_id", "client_id", @@ -234,6 +249,13 @@ var ( ` projections.idp_templates3_jwt.jwt_endpoint,` + ` projections.idp_templates3_jwt.keys_endpoint,` + ` projections.idp_templates3_jwt.header_name,` + + // azure + ` projections.idp_templates3_azure.idp_id,` + + ` projections.idp_templates3_azure.client_id,` + + ` projections.idp_templates3_azure.client_secret,` + + ` projections.idp_templates3_azure.scopes,` + + ` projections.idp_templates3_azure.tenant,` + + ` projections.idp_templates3_azure.is_email_verified,` + // github ` projections.idp_templates3_github.idp_id,` + ` projections.idp_templates3_github.client_id,` + @@ -291,6 +313,7 @@ var ( ` LEFT JOIN projections.idp_templates3_oauth2 ON projections.idp_templates3.id = projections.idp_templates3_oauth2.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_oauth2.instance_id` + ` LEFT JOIN projections.idp_templates3_oidc ON projections.idp_templates3.id = projections.idp_templates3_oidc.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_oidc.instance_id` + ` LEFT JOIN projections.idp_templates3_jwt ON projections.idp_templates3.id = projections.idp_templates3_jwt.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_jwt.instance_id` + + ` LEFT JOIN projections.idp_templates3_azure ON projections.idp_templates3.id = projections.idp_templates3_azure.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_azure.instance_id` + ` LEFT JOIN projections.idp_templates3_github ON projections.idp_templates3.id = projections.idp_templates3_github.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_github.instance_id` + ` LEFT JOIN projections.idp_templates3_github_enterprise ON projections.idp_templates3.id = projections.idp_templates3_github_enterprise.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_github_enterprise.instance_id` + ` LEFT JOIN projections.idp_templates3_gitlab ON projections.idp_templates3.id = projections.idp_templates3_gitlab.idp_id AND projections.idp_templates3.instance_id = projections.idp_templates3_gitlab.instance_id` + @@ -333,6 +356,13 @@ var ( "jwt_endpoint", "keys_endpoint", "header_name", + // azure + "idp_id", + "client_id", + "client_secret", + "scopes", + "tenant", + "is_email_verified", // github config "idp_id", "client_id", @@ -460,6 +490,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -583,6 +620,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -703,6 +747,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "jwt", "keys", "header", + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -823,6 +874,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github "idp-id", "client_id", @@ -942,6 +1000,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1061,6 +1126,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1181,6 +1253,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1300,6 +1379,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1438,6 +1524,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1587,6 +1680,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1734,6 +1834,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1856,6 +1963,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -1944,6 +2058,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -2032,6 +2153,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -2120,6 +2248,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, @@ -2208,6 +2343,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "jwt", "keys", "header", + // azure + nil, + nil, + nil, + nil, + nil, + nil, // github nil, nil, diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index 59217b4352..3f6d43dadf 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -21,6 +21,7 @@ const ( IDPTemplateOAuthTable = IDPTemplateTable + "_" + IDPTemplateOAuthSuffix IDPTemplateOIDCTable = IDPTemplateTable + "_" + IDPTemplateOIDCSuffix IDPTemplateJWTTable = IDPTemplateTable + "_" + IDPTemplateJWTSuffix + IDPTemplateAzureADTable = IDPTemplateTable + "_" + IDPTemplateAzureADSuffix IDPTemplateGitHubTable = IDPTemplateTable + "_" + IDPTemplateGitHubSuffix IDPTemplateGitHubEnterpriseTable = IDPTemplateTable + "_" + IDPTemplateGitHubEnterpriseSuffix IDPTemplateGitLabTable = IDPTemplateTable + "_" + IDPTemplateGitLabSuffix @@ -31,6 +32,7 @@ const ( IDPTemplateOAuthSuffix = "oauth2" IDPTemplateOIDCSuffix = "oidc" IDPTemplateJWTSuffix = "jwt" + IDPTemplateAzureADSuffix = "azure" IDPTemplateGitHubSuffix = "github" IDPTemplateGitHubEnterpriseSuffix = "github_enterprise" IDPTemplateGitLabSuffix = "gitlab" @@ -78,6 +80,14 @@ const ( JWTKeysEndpointCol = "keys_endpoint" JWTHeaderNameCol = "header_name" + AzureADIDCol = "idp_id" + AzureADInstanceIDCol = "instance_id" + AzureADClientIDCol = "client_id" + AzureADClientSecretCol = "client_secret" + AzureADScopesCol = "scopes" + AzureADTenantCol = "tenant" + AzureADIsEmailVerified = "is_email_verified" + GitHubIDCol = "idp_id" GitHubInstanceIDCol = "instance_id" GitHubClientIDCol = "client_id" @@ -206,6 +216,19 @@ func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerC IDPTemplateJWTSuffix, crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()), ), + crdb.NewSuffixedTable([]*crdb.Column{ + crdb.NewColumn(AzureADIDCol, crdb.ColumnTypeText), + crdb.NewColumn(AzureADInstanceIDCol, crdb.ColumnTypeText), + crdb.NewColumn(AzureADClientIDCol, crdb.ColumnTypeText), + crdb.NewColumn(AzureADClientSecretCol, crdb.ColumnTypeJSONB), + crdb.NewColumn(AzureADScopesCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(AzureADTenantCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(AzureADIsEmailVerified, crdb.ColumnTypeBool, crdb.Default(false)), + }, + crdb.NewPrimaryKey(AzureADInstanceIDCol, AzureADIDCol), + IDPTemplateAzureADSuffix, + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()), + ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(GitHubIDCol, crdb.ColumnTypeText), crdb.NewColumn(GitHubInstanceIDCol, crdb.ColumnTypeText), @@ -352,6 +375,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { Event: instance.IDPJWTConfigChangedEventType, Reduce: p.reduceOldJWTConfigChanged, }, + { + Event: instance.AzureADIDPAddedEventType, + Reduce: p.reduceAzureADIDPAdded, + }, + { + Event: instance.AzureADIDPChangedEventType, + Reduce: p.reduceAzureADIDPChanged, + }, { Event: instance.GitHubIDPAddedEventType, Reduce: p.reduceGitHubIDPAdded, @@ -429,7 +460,6 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { Event: org.OIDCIDPChangedEventType, Reduce: p.reduceOIDCIDPChanged, }, - { Event: org.JWTIDPAddedEventType, Reduce: p.reduceJWTIDPAdded, @@ -462,6 +492,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { Event: org.IDPJWTConfigChangedEventType, Reduce: p.reduceOldJWTConfigChanged, }, + { + Event: org.AzureADIDPAddedEventType, + Reduce: p.reduceAzureADIDPAdded, + }, + { + Event: org.AzureADIDPChangedEventType, + Reduce: p.reduceAzureADIDPChanged, + }, { Event: org.GitHubIDPAddedEventType, Reduce: p.reduceGitHubIDPAdded, @@ -1049,6 +1087,96 @@ func (p *idpTemplateProjection) reduceOldJWTConfigChanged(event eventstore.Event ), nil } +func (p *idpTemplateProjection) reduceAzureADIDPAdded(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.AzureADIDPAddedEvent + var idpOwnerType domain.IdentityProviderType + switch e := event.(type) { + case *org.AzureADIDPAddedEvent: + idpEvent = e.AzureADIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeOrg + case *instance.AzureADIDPAddedEvent: + idpEvent = e.AzureADIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeSystem + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-x9a022b", "reduce.wrong.event.type %v", []eventstore.EventType{org.AzureADIDPAddedEventType, instance.AzureADIDPAddedEventType}) + } + + 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.IDPTypeAzureAD), + 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(AzureADIDCol, idpEvent.ID), + handler.NewCol(AzureADInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(AzureADClientIDCol, idpEvent.ClientID), + handler.NewCol(AzureADClientSecretCol, idpEvent.ClientSecret), + handler.NewCol(AzureADScopesCol, database.StringArray(idpEvent.Scopes)), + handler.NewCol(AzureADTenantCol, idpEvent.Tenant), + handler.NewCol(AzureADIsEmailVerified, idpEvent.IsEmailVerified), + }, + crdb.WithTableSuffix(IDPTemplateAzureADSuffix), + ), + ), nil +} + +func (p *idpTemplateProjection) reduceAzureADIDPChanged(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.AzureADIDPChangedEvent + switch e := event.(type) { + case *org.AzureADIDPChangedEvent: + idpEvent = e.AzureADIDPChangedEvent + case *instance.AzureADIDPChangedEvent: + idpEvent = e.AzureADIDPChangedEvent + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.AzureADIDPChangedEventType, instance.AzureADIDPChangedEventType}) + } + + 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), + }, + ), + ) + githubCols := reduceAzureADIDPChangedColumns(idpEvent) + if len(githubCols) > 0 { + ops = append(ops, + crdb.AddUpdateStatement( + githubCols, + []handler.Condition{ + handler.NewCond(AzureADIDCol, idpEvent.ID), + handler.NewCond(AzureADInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(IDPTemplateAzureADSuffix), + ), + ) + } + + return crdb.NewMultiStatement( + &idpEvent, + ops..., + ), nil +} + func (p *idpTemplateProjection) reduceGitHubIDPAdded(event eventstore.Event) (*handler.Statement, error) { var idpEvent idp.GitHubIDPAddedEvent var idpOwnerType domain.IdentityProviderType @@ -1723,6 +1851,26 @@ func reduceJWTIDPChangedColumns(idpEvent idp.JWTIDPChangedEvent) []handler.Colum return jwtCols } +func reduceAzureADIDPChangedColumns(idpEvent idp.AzureADIDPChangedEvent) []handler.Column { + azureADCols := make([]handler.Column, 0, 5) + if idpEvent.ClientID != nil { + azureADCols = append(azureADCols, handler.NewCol(AzureADClientIDCol, *idpEvent.ClientID)) + } + if idpEvent.ClientSecret != nil { + azureADCols = append(azureADCols, handler.NewCol(AzureADClientSecretCol, *idpEvent.ClientSecret)) + } + if idpEvent.Scopes != nil { + azureADCols = append(azureADCols, handler.NewCol(AzureADScopesCol, database.StringArray(idpEvent.Scopes))) + } + if idpEvent.Tenant != nil { + azureADCols = append(azureADCols, handler.NewCol(AzureADTenantCol, *idpEvent.Tenant)) + } + if idpEvent.IsEmailVerified != nil { + azureADCols = append(azureADCols, handler.NewCol(AzureADIsEmailVerified, *idpEvent.IsEmailVerified)) + } + return azureADCols +} + func reduceGitHubIDPChangedColumns(idpEvent idp.GitHubIDPChangedEvent) []handler.Column { oauthCols := make([]handler.Column, 0, 3) if idpEvent.ClientID != nil { diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index 00fc50cb0e..f453575b33 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -410,6 +410,330 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { } } +func TestIDPTemplateProjection_reducesAzureAD(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 reduceAzureADIDPAdded minimal", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.AzureADIDPAddedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + } +}`), + ), instance.AzureADIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceAzureADIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateInsertStmt, + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "name", + domain.IdentityProviderTypeSystem, + domain.IDPTypeAzureAD, + false, + false, + false, + false, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates3_azure (idp_id, instance_id, client_id, client_secret, scopes, tenant, is_email_verified) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray(nil), + "", + false, + }, + }, + }, + }, + }, + }, + { + name: "instance reduceAzureADIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.AzureADIDPAddedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "tenant": "tenant", + "isEmailVerified": true, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.AzureADIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceAzureADIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateInsertStmt, + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "name", + domain.IdentityProviderTypeSystem, + domain.IDPTypeAzureAD, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates3_azure (idp_id, instance_id, client_id, client_secret, scopes, tenant, is_email_verified) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + "tenant", + true, + }, + }, + }, + }, + }, + }, + { + name: "org reduceAzureADIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.AzureADIDPAddedEventType), + org.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "tenant": "tenant", + "isEmailVerified": true, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), org.AzureADIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceAzureADIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateInsertStmt, + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "name", + domain.IdentityProviderTypeOrg, + domain.IDPTypeAzureAD, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates3_azure (idp_id, instance_id, client_id, client_secret, scopes, tenant, is_email_verified) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + "tenant", + true, + }, + }, + }, + }, + }, + }, + { + name: "instance reduceAzureADIDPChanged minimal", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.AzureADIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "isCreationAllowed": true, + "client_id": "id" +}`), + ), instance.AzureADIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceAzureADIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateUpdateMinimalStmt, + expectedArgs: []interface{}{ + true, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates3_azure SET client_id = $1 WHERE (idp_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "id", + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceAzureADIDPChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.AzureADIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "tenant": "tenant", + "isEmailVerified": true, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.AzureADIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceAzureADIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateUpdateStmt, + expectedArgs: []interface{}{ + "name", + true, + true, + true, + true, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates3_azure SET (client_id, client_secret, scopes, tenant, is_email_verified) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)", + expectedArgs: []interface{}{ + "client_id", + anyArg{}, + database.StringArray{"profile"}, + "tenant", + true, + "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_reducesGitHub(t *testing.T) { type args struct { event func(t *testing.T) eventstore.Event diff --git a/internal/repository/idp/azuread.go b/internal/repository/idp/azuread.go new file mode 100644 index 0000000000..e986801f27 --- /dev/null +++ b/internal/repository/idp/azuread.go @@ -0,0 +1,164 @@ +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 AzureADIDPAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name string `json:"name,omitempty"` + ClientID string `json:"client_id,omitempty"` + ClientSecret *crypto.CryptoValue `json:"client_secret,omitempty"` + Scopes []string `json:"scopes,omitempty"` + Tenant string `json:"tenant,omitempty"` + IsEmailVerified bool `json:"isEmailVerified,omitempty"` + Options +} + +func NewAzureADIDPAddedEvent( + base *eventstore.BaseEvent, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + tenant string, + isEmailVerified bool, + options Options, +) *AzureADIDPAddedEvent { + return &AzureADIDPAddedEvent{ + BaseEvent: *base, + ID: id, + Name: name, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopes, + Tenant: tenant, + IsEmailVerified: isEmailVerified, + Options: options, + } +} + +func (e *AzureADIDPAddedEvent) Data() interface{} { + return e +} + +func (e *AzureADIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func AzureADIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &AzureADIDPAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-Grh2g", "unable to unmarshal event") + } + + return e, nil +} + +type AzureADIDPChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name *string `json:"name,omitempty"` + ClientID *string `json:"client_id,omitempty"` + ClientSecret *crypto.CryptoValue `json:"client_secret,omitempty"` + Scopes []string `json:"scopes,omitempty"` + Tenant *string `json:"tenant,omitempty"` + IsEmailVerified *bool `json:"isEmailVerified,omitempty"` + OptionChanges +} + +func NewAzureADIDPChangedEvent( + base *eventstore.BaseEvent, + id string, + changes []AzureADIDPChanges, +) (*AzureADIDPChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "IDP-BH3dl", "Errors.NoChangesFound") + } + changedEvent := &AzureADIDPChangedEvent{ + BaseEvent: *base, + ID: id, + } + for _, change := range changes { + change(changedEvent) + } + return changedEvent, nil +} + +type AzureADIDPChanges func(*AzureADIDPChangedEvent) + +func ChangeAzureADName(name string) func(*AzureADIDPChangedEvent) { + return func(e *AzureADIDPChangedEvent) { + e.Name = &name + } +} + +func ChangeAzureADClientID(clientID string) func(*AzureADIDPChangedEvent) { + return func(e *AzureADIDPChangedEvent) { + e.ClientID = &clientID + } +} + +func ChangeAzureADClientSecret(clientSecret *crypto.CryptoValue) func(*AzureADIDPChangedEvent) { + return func(e *AzureADIDPChangedEvent) { + e.ClientSecret = clientSecret + } +} + +func ChangeAzureADOptions(options OptionChanges) func(*AzureADIDPChangedEvent) { + return func(e *AzureADIDPChangedEvent) { + e.OptionChanges = options + } +} + +func ChangeAzureADScopes(scopes []string) func(*AzureADIDPChangedEvent) { + return func(e *AzureADIDPChangedEvent) { + e.Scopes = scopes + } +} + +func ChangeAzureADTenant(tenant string) func(*AzureADIDPChangedEvent) { + return func(e *AzureADIDPChangedEvent) { + e.Tenant = &tenant + } +} + +func ChangeAzureADIsEmailVerified(isEmailVerified bool) func(*AzureADIDPChangedEvent) { + return func(e *AzureADIDPChangedEvent) { + e.IsEmailVerified = &isEmailVerified + } +} + +func (e *AzureADIDPChangedEvent) Data() interface{} { + return e +} + +func (e *AzureADIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func AzureADIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &AzureADIDPChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-D3gjzh", "unable to unmarshal event") + } + + return e, nil +} diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 60417121e8..b6d8923ecf 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -76,6 +76,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, OIDCIDPChangedEventType, OIDCIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPAddedEventType, JWTIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPChangedEventType, JWTIDPChangedEventMapper). + RegisterFilterEventMapper(AggregateType, AzureADIDPAddedEventType, AzureADIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, AzureADIDPChangedEventType, AzureADIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GitHubIDPAddedEventType, GitHubIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, GitHubIDPChangedEventType, GitHubIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GitHubEnterpriseIDPAddedEventType, GitHubEnterpriseIDPAddedEventMapper). diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index 22aa4c41e3..44377331c4 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -16,6 +16,8 @@ const ( OIDCIDPChangedEventType eventstore.EventType = "instance.idp.oidc.changed" JWTIDPAddedEventType eventstore.EventType = "instance.idp.jwt.added" JWTIDPChangedEventType eventstore.EventType = "instance.idp.jwt.changed" + AzureADIDPAddedEventType eventstore.EventType = "instance.idp.azure.added" + AzureADIDPChangedEventType eventstore.EventType = "instance.idp.azure.changed" GitHubIDPAddedEventType eventstore.EventType = "instance.idp.github.added" GitHubIDPChangedEventType eventstore.EventType = "instance.idp.github.changed" GitHubEnterpriseIDPAddedEventType eventstore.EventType = "instance.idp.github_enterprise.added" @@ -271,6 +273,86 @@ func JWTIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) return &JWTIDPChangedEvent{JWTIDPChangedEvent: *e.(*idp.JWTIDPChangedEvent)}, nil } +type AzureADIDPAddedEvent struct { + idp.AzureADIDPAddedEvent +} + +func NewAzureADIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + tenant string, + isEmailVerified bool, + options idp.Options, +) *AzureADIDPAddedEvent { + + return &AzureADIDPAddedEvent{ + AzureADIDPAddedEvent: *idp.NewAzureADIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + AzureADIDPAddedEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + tenant, + isEmailVerified, + options, + ), + } +} + +func AzureADIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.AzureADIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &AzureADIDPAddedEvent{AzureADIDPAddedEvent: *e.(*idp.AzureADIDPAddedEvent)}, nil +} + +type AzureADIDPChangedEvent struct { + idp.AzureADIDPChangedEvent +} + +func NewAzureADIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.AzureADIDPChanges, +) (*AzureADIDPChangedEvent, error) { + + changedEvent, err := idp.NewAzureADIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + AzureADIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &AzureADIDPChangedEvent{AzureADIDPChangedEvent: *changedEvent}, nil +} + +func AzureADIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.AzureADIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &AzureADIDPChangedEvent{AzureADIDPChangedEvent: *e.(*idp.AzureADIDPChangedEvent)}, nil +} + type GitHubIDPAddedEvent struct { idp.GitHubIDPAddedEvent } diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index fc6128f7b1..fb85ca86ee 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -84,6 +84,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, OIDCIDPChangedEventType, OIDCIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPAddedEventType, JWTIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPChangedEventType, JWTIDPChangedEventMapper). + RegisterFilterEventMapper(AggregateType, AzureADIDPAddedEventType, AzureADIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, AzureADIDPChangedEventType, AzureADIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GitHubIDPAddedEventType, GitHubIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, GitHubIDPChangedEventType, GitHubIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GitHubEnterpriseIDPAddedEventType, GitHubEnterpriseIDPAddedEventMapper). diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index db9ad882f9..2ed3a68dc8 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -16,6 +16,8 @@ const ( OIDCIDPChangedEventType eventstore.EventType = "org.idp.oidc.changed" JWTIDPAddedEventType eventstore.EventType = "org.idp.jwt.added" JWTIDPChangedEventType eventstore.EventType = "org.idp.jwt.changed" + AzureADIDPAddedEventType eventstore.EventType = "org.idp.azure.added" + AzureADIDPChangedEventType eventstore.EventType = "org.idp.azure.changed" GitHubIDPAddedEventType eventstore.EventType = "org.idp.github.added" GitHubIDPChangedEventType eventstore.EventType = "org.idp.github.changed" GitHubEnterpriseIDPAddedEventType eventstore.EventType = "org.idp.github_enterprise.added" @@ -271,6 +273,86 @@ func JWTIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) return &JWTIDPChangedEvent{JWTIDPChangedEvent: *e.(*idp.JWTIDPChangedEvent)}, nil } +type AzureADIDPAddedEvent struct { + idp.AzureADIDPAddedEvent +} + +func NewAzureADIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + tenant string, + isEmailVerified bool, + options idp.Options, +) *AzureADIDPAddedEvent { + + return &AzureADIDPAddedEvent{ + AzureADIDPAddedEvent: *idp.NewAzureADIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + AzureADIDPAddedEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + tenant, + isEmailVerified, + options, + ), + } +} + +func AzureADIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.AzureADIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &AzureADIDPAddedEvent{AzureADIDPAddedEvent: *e.(*idp.AzureADIDPAddedEvent)}, nil +} + +type AzureADIDPChangedEvent struct { + idp.AzureADIDPChangedEvent +} + +func NewAzureADIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.AzureADIDPChanges, +) (*AzureADIDPChangedEvent, error) { + + changedEvent, err := idp.NewAzureADIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + AzureADIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &AzureADIDPChangedEvent{AzureADIDPChangedEvent: *changedEvent}, nil +} + +func AzureADIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.AzureADIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &AzureADIDPChangedEvent{AzureADIDPChangedEvent: *e.(*idp.AzureADIDPChangedEvent)}, nil +} + type GitHubIDPAddedEvent struct { idp.GitHubIDPAddedEvent } diff --git a/pkg/grpc/idp/idp.go b/pkg/grpc/idp/idp.go index ac3ab5e769..6ff8081b42 100644 --- a/pkg/grpc/idp/idp.go +++ b/pkg/grpc/idp/idp.go @@ -1,3 +1,4 @@ package idp type IDPConfig = isIDP_Config +type IsAzureADTenantType = isAzureADTenant_Type diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index f852cbe18d..6d34e8b8ec 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1320,6 +1320,30 @@ service AdminService { }; } + // Add a new Azure AD identity provider on the instance + rpc AddAzureADProvider(AddAzureADProviderRequest) returns (AddAzureADProviderResponse) { + option (google.api.http) = { + post: "/idps/azure" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + } + + // Change an existing Azure AD identity provider on the instance + rpc UpdateAzureADProvider(UpdateAzureADProviderRequest) returns (UpdateAzureADProviderResponse) { + option (google.api.http) = { + put: "/idps/azure/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + } + // Add a new GitHub identity provider on the instance rpc AddGitHubProvider(AddGitHubProviderRequest) returns (AddGitHubProviderResponse) { option (google.api.http) = { @@ -4525,6 +4549,39 @@ message UpdateJWTProviderResponse { zitadel.v1.ObjectDetails details = 1; } +message AddAzureADProviderRequest { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_secret = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // if not provided the `common` tenant will be used + zitadel.idp.v1.AzureADTenant tenant = 4; + bool email_verified = 5; + repeated string scopes = 6 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 7; +} + +message AddAzureADProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateAzureADProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // client_secret will only be updated if provided + string client_secret = 4 [(validate.rules).string = {max_len: 200}]; + // if not provided the `common` tenant will be used + zitadel.idp.v1.AzureADTenant tenant = 5; + bool email_verified = 6; + repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 8; +} + +message UpdateAzureADProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddGitHubProviderRequest { // GitHub will be used as default, if no name is provided string name = 1 [(validate.rules).string = {max_len: 200}]; diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 414d23347f..4f3a583c3d 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -271,8 +271,10 @@ message ProviderConfig { GitHubEnterpriseServerConfig github_es = 8; GitLabConfig gitlab = 9; GitLabSelfHostedConfig gitlab_self_hosted = 10; + AzureADConfig azure_ad = 11; } } + message OAuthConfig { string client_id = 1; string authorization_endpoint = 2; @@ -329,6 +331,13 @@ message LDAPConfig { Options provider_options = 9; } +message AzureADConfig { + string client_id = 1; + AzureADTenant tenant = 2; + bool email_verified = 3; + repeated string scopes = 4; +} + message Options { bool is_linking_allowed = 1; bool is_creation_allowed = 2; @@ -352,3 +361,15 @@ message LDAPAttributes { string profile_attribute = 13 [(validate.rules).string = {max_len: 200}]; } +enum AzureADTenantType { + AZURE_AD_TENANT_TYPE_COMMON = 0; + AZURE_AD_TENANT_TYPE_ORGANISATIONS = 1; + AZURE_AD_TENANT_TYPE_CONSUMERS = 2; +} + +message AzureADTenant { + oneof type { + AzureADTenantType tenant_type = 1; + string tenant_id = 2; + } +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 208c3584b0..e56247ad38 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -6536,6 +6536,30 @@ service ManagementService { }; } + // Add a new Azure AD identity provider in the organisation + rpc AddAzureADProvider(AddAzureADProviderRequest) returns (AddAzureADProviderResponse) { + option (google.api.http) = { + post: "/idps/azure" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + } + + // Change an existing Azure AD identity provider in the organisation + rpc UpdateAzureADProvider(UpdateAzureADProviderRequest) returns (UpdateAzureADProviderResponse) { + option (google.api.http) = { + put: "/idps/azure/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + } + // Add a new GitHub identity provider in the organization rpc AddGitHubProvider(AddGitHubProviderRequest) returns (AddGitHubProviderResponse) { option (google.api.http) = { @@ -11199,6 +11223,39 @@ message UpdateJWTProviderResponse { zitadel.v1.ObjectDetails details = 1; } +message AddAzureADProviderRequest { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_secret = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // if not provided the `common` tenant will be used + zitadel.idp.v1.AzureADTenant tenant = 4; + bool email_verified = 5; + repeated string scopes = 6 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 7; +} + +message AddAzureADProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateAzureADProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string client_id = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // client_secret will only be updated if provided + string client_secret = 4 [(validate.rules).string = {max_len: 200}]; + // if not provided the `common` tenant will be used + zitadel.idp.v1.AzureADTenant tenant = 5; + bool email_verified = 6; + repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; + zitadel.idp.v1.Options provider_options = 8; +} + +message UpdateAzureADProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddGitHubProviderRequest { // GitHub will be used as default, if no name is provided string name = 1 [(validate.rules).string = {max_len: 200}];