diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index d0907d6e1..1ba6fc203 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -466,3 +466,14 @@ func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProvide Details: object_pb.DomainToChangeDetailsPb(details), }, nil } + +func (s *Server) AddZitadelProvider(ctx context.Context, req *admin_pb.AddZitadelProviderRequest) (*admin_pb.AddZitadelProviderResponse, error) { + id, details, err := s.command.AddInstanceZitadelProvider(ctx, addZitadelProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.AddZitadelProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index f158f128b..7383ba88f 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -1,6 +1,8 @@ package admin import ( + "strings" + "github.com/crewjam/saml" "github.com/muhlemmer/gu" dsig "github.com/russellhaering/goxmldsig" @@ -11,6 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/zerrors" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" @@ -545,3 +548,22 @@ func signatureAlgorithmToCommand(signatureAlgorithm idp_pb.SAMLSignatureAlgorith return "" } } + +func addZitadelProviderToCommand(req *admin_pb.AddZitadelProviderRequest) command.ZitadelProvider { + instanceRolesInfo := make([]idp.RolesInfo, 0, len(req.InstanceRolesInfo)) + for _, info := range req.InstanceRolesInfo { + instanceRolesInfo = append(instanceRolesInfo, idp.RolesInfo{ + OrganizationID: strings.TrimSpace(info.OrganizationId), + OrganizationDomain: strings.TrimSpace(info.OrganizationDomain), + }) + } + return command.ZitadelProvider{ + Name: req.Name, + Issuer: req.Issuer, + ClientID: req.ClientId, + ClientSecret: req.ClientSecret, + Scopes: req.Scopes, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + InstanceRolesInfo: instanceRolesInfo, + } +} diff --git a/internal/api/grpc/admin/idp_converter_test.go b/internal/api/grpc/admin/idp_converter_test.go index adad5f348..e5b79b836 100644 --- a/internal/api/grpc/admin/idp_converter_test.go +++ b/internal/api/grpc/admin/idp_converter_test.go @@ -3,11 +3,14 @@ package admin import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/test" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" - "github.com/zitadel/zitadel/pkg/grpc/idp" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" ) func Test_addOIDCIDPRequestToDomain(t *testing.T) { @@ -23,13 +26,13 @@ func Test_addOIDCIDPRequestToDomain(t *testing.T) { args: args{ req: &admin_pb.AddOIDCIDPRequest{ Name: "ZITADEL", - StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE, + StylingType: idp_pb.IDPStylingType_STYLING_TYPE_GOOGLE, ClientId: "test1234", ClientSecret: "test4321", Issuer: "zitadel.ch", Scopes: []string{"email", "profile"}, - DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, - UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, + DisplayNameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, + UsernameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, AutoRegister: true, }, }, @@ -70,8 +73,8 @@ func Test_addOIDCIDPRequestToDomainOIDCIDPConfig(t *testing.T) { ClientSecret: "test4321", Issuer: "zitadel.ch", Scopes: []string{"email", "profile"}, - DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, - UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, + DisplayNameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, + UsernameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, }, }, }, @@ -104,7 +107,7 @@ func Test_updateIDPToDomain(t *testing.T) { req: &admin_pb.UpdateIDPRequest{ IdpId: "13523", Name: "new name", - StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE, + StylingType: idp_pb.IDPStylingType_STYLING_TYPE_GOOGLE, AutoRegister: true, }, }, @@ -141,8 +144,8 @@ func Test_updateOIDCConfigToDomain(t *testing.T) { ClientId: "ZITEADEL", ClientSecret: "i'm so secret", Scopes: []string{"profile"}, - DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, - UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, + DisplayNameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, + UsernameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, }, }, }, @@ -164,7 +167,7 @@ func Test_signatureAlgorithmToCommand(t *testing.T) { t.Parallel() tests := []struct { name string - signatureAlgorithm idp.SAMLSignatureAlgorithm + signatureAlgorithm idp_pb.SAMLSignatureAlgorithm wantSignatureAlgorithm string }{ { @@ -174,17 +177,17 @@ func Test_signatureAlgorithmToCommand(t *testing.T) { }, { name: "RSA_SHA1", - signatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA1, + signatureAlgorithm: idp_pb.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA1, wantSignatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", }, { name: "RSA_SHA256", - signatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA256, + signatureAlgorithm: idp_pb.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA256, wantSignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", }, { name: "RSA_SHA512", - signatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA512, + signatureAlgorithm: idp_pb.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA512, wantSignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", }, } @@ -196,3 +199,69 @@ func Test_signatureAlgorithmToCommand(t *testing.T) { }) } } + +func Test_addZitadelProviderToCommand(t *testing.T) { + t.Parallel() + tests := []struct { + name string + req *admin_pb.AddZitadelProviderRequest + want command.ZitadelProvider + }{ + { + name: "all fields filled", + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"}, + ProviderOptions: &idp_pb.Options{ + IsLinkingAllowed: false, + IsCreationAllowed: true, + IsAutoCreation: false, + IsAutoUpdate: true, + AutoLinking: 0, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + { + + OrganizationId: "org2", + OrganizationDomain: "org2.com", + }, + }, + }, + want: command.ZitadelProvider{ + Name: "Zitadel Support IdP", + ClientID: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsAutoCreation: false, + IsLinkingAllowed: false, + IsAutoUpdate: true, + }, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "org2.com", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := addZitadelProviderToCommand(tt.req) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/admin/integration_test/idp_test.go b/internal/api/grpc/admin/integration_test/idp_test.go new file mode 100644 index 000000000..944b2bdc7 --- /dev/null +++ b/internal/api/grpc/admin/integration_test/idp_test.go @@ -0,0 +1,198 @@ +//go:build integration + +package admin_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object" +) + +func Test_AddZitadelProvider(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.AddZitadelProviderRequest + } + tests := []struct { + name string + args args + wantErr error + wantResponse *admin_pb.AddZitadelProviderResponse + }{ + { + name: "missing required field: name", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{}, + }, + wantErr: status.Error(codes.InvalidArgument, "invalid AddZitadelProviderRequest.Name: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "missing required field: issuer", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + }, + }, + wantErr: status.Error(codes.InvalidArgument, "invalid AddZitadelProviderRequest.Issuer: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "missing required field: client_id", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + Issuer: "zitadel.example.com", + }, + }, + wantErr: status.Error(codes.InvalidArgument, "invalid AddZitadelProviderRequest.ClientId: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "missing required field: client_secret", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + Issuer: "zitadel.example.com", + ClientId: "test-client", + }, + }, + wantErr: status.Error( + codes.InvalidArgument, + "invalid AddZitadelProviderRequest.ClientSecret: value length must be between 1 and 1000 runes, inclusive", + ), + }, + { + name: "missing org ID in instance roles info", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "", + }, + }, + }, + }, + wantErr: status.Error( + codes.InvalidArgument, + "invalid AddZitadelProviderRequest.InstanceRolesInfo[0]: embedded message failed validation | caused by: invalid InstanceRolesInfo.OrganizationId: value length must be between 1 and 200 runes, inclusive", + ), + }, + { + name: "missing org domain in instance roles info", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + { + OrganizationId: "org2", + }, + }, + }, + }, + wantErr: status.Error( + codes.InvalidArgument, + "invalid AddZitadelProviderRequest.InstanceRolesInfo[1]: embedded message failed validation | caused by: invalid InstanceRolesInfo.OrganizationDomain: value length must be between 1 and 200 runes, inclusive", + ), + }, + { + name: "valid request without instance roles info", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + }, + }, + }, + wantResponse: &admin_pb.AddZitadelProviderResponse{ + Details: &object_pb.ObjectDetails{ + ResourceOwner: Instance.Instance.Id, + }, + }, + }, + { + name: "valid request with instance roles info", + args: args{ + ctx: AdminCTX, + req: &admin_pb.AddZitadelProviderRequest{ + Name: "Zitadel Support IdP", + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + wantResponse: &admin_pb.AddZitadelProviderResponse{ + Details: &object_pb.ObjectDetails{ + ResourceOwner: Instance.Instance.Id, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + before := time.Now() + got, err := Client.AddZitadelProvider(tt.args.ctx, tt.args.req) + after := time.Now() + if tt.wantErr != nil { + require.Error(t, err) + grpcStatus, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, status.Code(tt.wantErr), grpcStatus.Code()) + assert.Equal(t, status.Convert(tt.wantErr).Message(), grpcStatus.Message()) + return + } + require.NoError(t, err) + assert.NotNil(t, got) + assert.NotEmpty(t, got.GetId()) + assert.WithinRange(t, got.GetDetails().GetCreationDate().AsTime(), before, after) + assert.Equal(t, tt.wantResponse.GetDetails().GetResourceOwner(), got.GetDetails().GetResourceOwner()) + }) + } +} diff --git a/internal/command/idp.go b/internal/command/idp.go index b53a6610e..1585528a8 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -137,6 +137,16 @@ type AppleProvider struct { IDPOptions idp.Options } +type ZitadelProvider struct { + Name string + Issuer string + ClientID string + ClientSecret string + Scopes []string + IDPOptions idp.Options + InstanceRolesInfo []idp.RolesInfo +} + // ExistsIDPOnOrgOrInstance query first org level IDPs and then instance level IDPs, no check if the IDP is active func ExistsIDPOnOrgOrInstance(ctx context.Context, filter preparation.FilterToQueryReducer, instanceID, orgID, id string) (exists bool, err error) { ctx, span := tracing.NewSpan(ctx) diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index d1cb0d592..fdfd9e21c 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -2322,3 +2322,46 @@ func (wm *AllIDPWriteModel) ToSAMLProvider(callbackURL string, idpAlg crypto.Enc } return wm.samlModel.ToProvider(callbackURL, idpAlg, getRequest, addRequest) } + +type ZitadelIDPWriteModel struct { + eventstore.WriteModel + + Name string + ID string + Issuer string + ClientID string + ClientSecret *crypto.CryptoValue + Scopes []string + idp.Options + InstanceRolesInfo []idp.RolesInfo + + State domain.IDPState +} + +func (wm *ZitadelIDPWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *idp.ZitadelIDPAddedEvent: + wm.reduceAddedEvent(e) + case *idp.ZitadelIDPChangedEvent: + // todo (@grvijayan): to please the linter for now + wm.reduceChangedEvent(e) + } + } + return wm.WriteModel.Reduce() +} + +func (wm *ZitadelIDPWriteModel) reduceAddedEvent(e *idp.ZitadelIDPAddedEvent) { + wm.Name = e.Name + wm.Issuer = e.Issuer + wm.ClientID = e.ClientID + wm.ClientSecret = e.ClientSecret + wm.Scopes = e.Scopes + wm.Options = e.Options + wm.InstanceRolesInfo = e.InstanceRolesInfo + wm.State = domain.IDPStateActive +} + +func (wm *ZitadelIDPWriteModel) reduceChangedEvent(e *idp.ZitadelIDPChangedEvent) { + // todo (@grvijayan): will be implemented along with UpdateZitadelProvider changes +} diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 419e0436e..a7a805e68 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -1937,3 +1937,89 @@ func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id strin }, nil } } + +func (c *Commands) AddInstanceZitadelProvider(ctx context.Context, provider ZitadelProvider) (string, *domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + + err = c.validateZitadelProvider(ctx, &provider) + if err != nil { + return "", nil, err + } + + writeModel := NewInstanceZitadelIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceZitadelProvider(instanceAgg, writeModel, provider)) //nolint:staticcheck + 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) prepareAddInstanceZitadelProvider(a *instance.Aggregate, writeModel *InstanceZitadelIDPWriteModel, provider ZitadelProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + 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.NewZitadelIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.Issuer, + provider.ClientID, + secret, + provider.Scopes, + provider.IDPOptions, + provider.InstanceRolesInfo, + ), + }, nil + }, nil + } +} + +func (c *Commands) validateZitadelProvider(ctx context.Context, provider *ZitadelProvider) error { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return zerrors.ThrowInvalidArgument(nil, "INST-Sgtj5", "Errors.Invalid.Argument") + } + if provider.Issuer = strings.TrimSpace(provider.Issuer); provider.Issuer == "" { + return zerrors.ThrowInvalidArgument(nil, "INST-Hz6zj", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return zerrors.ThrowInvalidArgument(nil, "INST-fb5jm", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return zerrors.ThrowInvalidArgument(nil, "INST-Sfdf4", "Errors.Invalid.Argument") + } + + for i := range provider.InstanceRolesInfo { + provider.InstanceRolesInfo[i].OrganizationID = strings.TrimSpace(provider.InstanceRolesInfo[i].OrganizationID) + if provider.InstanceRolesInfo[i].OrganizationID == "" { + return zerrors.ThrowInvalidArgument(nil, "INST-9uEJaM", "Errors.Invalid.Argument") + } + provider.InstanceRolesInfo[i].OrganizationDomain = strings.TrimSpace(provider.InstanceRolesInfo[i].OrganizationDomain) + if provider.InstanceRolesInfo[i].OrganizationDomain == "" { + return zerrors.ThrowInvalidArgument(nil, "INST-y4Ib4D", "Errors.Invalid.Argument") + } + } + + return nil +} diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index 262b01655..00d98d5ef 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -1034,3 +1034,43 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { EventData(map[string]interface{}{"idpConfigId": wm.ID}). Builder() } + +type InstanceZitadelIDPWriteModel struct { + ZitadelIDPWriteModel +} + +func NewInstanceZitadelIDPWriteModel(instanceID, id string) *InstanceZitadelIDPWriteModel { + return &InstanceZitadelIDPWriteModel{ + ZitadelIDPWriteModel: ZitadelIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + }, + ID: id, + }, + } +} + +func (wm *InstanceZitadelIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *instance.ZitadelIDPAddedEvent: + wm.ZitadelIDPWriteModel.AppendEvents(&e.ZitadelIDPAddedEvent) + default: + wm.ZitadelIDPWriteModel.AppendEvents(e) + } + } +} + +func (wm *InstanceZitadelIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.ZitadelIDPAddedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index a5c172b42..517d62187 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -5937,3 +5937,312 @@ func TestCommandSide_RegenerateInstanceSAMLProviderCertificate(t *testing.T) { }) } } + +func TestCommandSide_AddInstanceZitadelIDP(t *testing.T) { + pushErr := errors.New("push error") + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + provider ZitadelProvider + } + 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: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: ZitadelProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Sgtj5", "")) + }, + }, + }, + { + "invalid issuer", + fields{ + eventstore: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: ZitadelProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Hz6zj", "")) + }, + }, + }, + { + "invalid clientID", + fields{ + eventstore: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-fb5jm", "")) + }, + }, + }, + { + "invalid clientSecret", + fields{ + eventstore: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Sfdf4", "")) + }, + }, + }, + { + name: "push error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPushFailed( + pushErr, + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + ClientSecret: "clientSecret", + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, pushErr) + }, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + ClientSecret: "clientSecret", + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "ok all set", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{openid.ScopeOpenID}, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: ZitadelProvider{ + Name: "name", + Issuer: "issuer", + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{openid.ScopeOpenID}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, + }, + }, + res: res{ + 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(t), + idGenerator: tt.fields.idGenerator, + idpConfigEncryption: tt.fields.secretCrypto, + } + idpId, got, err := c.AddInstanceZitadelProvider(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, idpId) + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/repository/idp/zitadel.go b/internal/repository/idp/zitadel.go new file mode 100644 index 000000000..c222fbbde --- /dev/null +++ b/internal/repository/idp/zitadel.go @@ -0,0 +1,61 @@ +package idp + +import ( + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/eventstore" +) + +type RolesInfo struct { + OrganizationID string `json:"organizationId"` + OrganizationDomain string `json:"organizationDomain"` +} + +type ZitadelIDPAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name string `json:"name"` + Issuer string `json:"issuer"` + ClientID string `json:"clientId"` + ClientSecret *crypto.CryptoValue `json:"clientSecret"` + Scopes []string `json:"scopes,omitempty"` + InstanceRolesInfo []RolesInfo `json:"instanceRolesInfo,omitempty"` + Options +} + +func NewZitadelIDPAddedEvent( + base *eventstore.BaseEvent, + id, + name, + issuer, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options Options, + instanceRolesInfo []RolesInfo, +) *ZitadelIDPAddedEvent { + return &ZitadelIDPAddedEvent{ + BaseEvent: *base, + ID: id, + Name: name, + Issuer: issuer, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopes, + Options: options, + InstanceRolesInfo: instanceRolesInfo, + } +} + +func (e *ZitadelIDPAddedEvent) Payload() interface{} { + return e +} + +func (e *ZitadelIDPAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +type ZitadelIDPChangedEvent struct { + eventstore.BaseEvent `json:"-"` + // todo (@grvijayan): will be added along with UpdateZitadelProvider changes +} diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 7610913c0..f9d7dd65b 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -105,6 +105,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SAMLIDPAddedEventType, SAMLIDPAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, SAMLIDPChangedEventType, SAMLIDPChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, ZitadelIDPAddedEventType, eventstore.GenericEventMapper[ZitadelIDPAddedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderCascadeRemovedEventType, IdentityProviderCascadeRemovedEventMapper) diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index 95ffb017c..9b9b1350f 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -38,6 +38,7 @@ const ( SAMLIDPAddedEventType eventstore.EventType = "instance.idp.saml.added" SAMLIDPChangedEventType eventstore.EventType = "instance.idp.saml.changed" IDPRemovedEventType eventstore.EventType = "instance.idp.removed" + ZitadelIDPAddedEventType eventstore.EventType = "instance.idp.zitadel.added" ) type OAuthIDPAddedEvent struct { @@ -1128,3 +1129,43 @@ func IDPRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) { return &IDPRemovedEvent{RemovedEvent: *e.(*idp.RemovedEvent)}, nil } + +type ZitadelIDPAddedEvent struct { + idp.ZitadelIDPAddedEvent +} + +func NewZitadelIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + issuer, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options idp.Options, + instanceRolesInfo []idp.RolesInfo, +) *ZitadelIDPAddedEvent { + + return &ZitadelIDPAddedEvent{ + ZitadelIDPAddedEvent: *idp.NewZitadelIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + ZitadelIDPAddedEventType, + ), + id, + name, + issuer, + clientID, + clientSecret, + scopes, + options, + instanceRolesInfo, + ), + } +} + +func (e *ZitadelIDPAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 2d5a250ca..f0e52857e 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -7338,13 +7338,9 @@ message AddZitadelProviderRequest { ]; zitadel.idp.v1.Options provider_options = 6; - bool is_id_token_mapping = 7; - // Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow. - bool use_pkce = 8; - // Optionally specify the details (organization_id, organization_domain) related to instance roles. // It is used to determine if a user should be assigned instance administrator roles after authenticating with the provider. - repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 9 [(validate.rules).repeated = {max_items: 20}]; + repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 7 [(validate.rules).repeated = {max_items: 20}]; } message AddZitadelProviderResponse { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index ed5d1c7f2..703d294b5 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -14087,13 +14087,9 @@ message AddZitadelProviderRequest { ]; zitadel.idp.v1.Options provider_options = 6; - bool is_id_token_mapping = 7; - // Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow. - bool use_pkce = 8; - // Optionally specify the details (organization_id, organization_domain) related to instance roles. // It is used to determine if a user should be assigned instance administrator roles after authenticating with the provider. - repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 9 [(validate.rules).repeated = {max_items: 20}]; + repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 7 [(validate.rules).repeated = {max_items: 20}]; } message AddZitadelProviderResponse {