From 77169181a391e1485cf27bd38ad800baaaac2142 Mon Sep 17 00:00:00 2001 From: Gayathri Vijayan <66356931+grvijayan@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:17:49 +0200 Subject: [PATCH] feat: get/list Zitadel IdP (#12394) # Which Problems Are Solved This PR extends the functionality of `GetProviderByID`, `ListProviders` v1 endpoints and `GetIDPByID` v2 endpoint to also return Zitadel IdP when queried. # How the Problems Are Solved - Add `PROVIDER_TYPE_ZITADEL` and a `ZitadelConfig` to `zitadel.idp.v1.ProviderConfig` (v1 APIs). - Add `IDP_TYPE_ZITADEL`, `ZitadelConfig`, `InstanceRolesInfo` in v2 `idp.proto` - Extend internal/query IDP template querying to include a `ZitadelIDPTemplate` (incl. issuer, client credentials, scopes, instance roles info). - Add/extend integration tests for updating + fetching providers by ID and listing providers. # Additional Changes N/A # Additional Context Closes https://github.com/zitadel/zitadel/issues/12051 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../grpc/admin/integration_test/idp_test.go | 407 ++++++++++++-- internal/api/grpc/idp/converter.go | 24 + .../idp/v2/integration_test/query_test.go | 96 ++++ internal/api/grpc/idp/v2/query.go | 24 + .../management/integration_test/idp_test.go | 529 ++++++++++++++++-- internal/command/idp_model.go | 4 +- internal/command/instance_idp_test.go | 15 +- internal/command/org_idp_test.go | 15 +- internal/integration/client.go | 42 ++ internal/query/idp_template.go | 110 +++- internal/query/idp_template_test.go | 495 +++++++++++++++- proto/zitadel/idp.proto | 28 + proto/zitadel/idp/v2/idp.proto | 48 ++ 13 files changed, 1725 insertions(+), 112 deletions(-) diff --git a/internal/api/grpc/admin/integration_test/idp_test.go b/internal/api/grpc/admin/integration_test/idp_test.go index e16f1b1b3..7e54e58b0 100644 --- a/internal/api/grpc/admin/integration_test/idp_test.go +++ b/internal/api/grpc/admin/integration_test/idp_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/zitadel/zitadel/internal/integration" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -227,41 +228,22 @@ func Test_AddZitadelProvider(t *testing.T) { } func Test_UpdateZitadelProvider(t *testing.T) { - existingProvider, err := Instance.Client.Admin.AddZitadelProvider(AdminCTX, &admin_pb.AddZitadelProviderRequest{ - Name: "Zitadel Support IdP", - Issuer: "zitadel.example.com", - ClientId: "test-client", - ClientSecret: "test-secret", - Scopes: []string{"email", "profile"}, - ProviderOptions: &idp_pb.Options{ - IsCreationAllowed: true, - }, - InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ - { - OrganizationId: "org1", - OrganizationDomain: "org1.com", - }, - }, - }) - require.NoError(t, err) - require.NotEmpty(t, existingProvider.GetId()) - type args struct { ctx context.Context req *admin_pb.UpdateZitadelProviderRequest } tests := []struct { - name string - args args - wantErr error - wantResponse *admin_pb.UpdateZitadelProviderResponse + name string + args args + wantErr error + wantResponse *admin_pb.UpdateZitadelProviderResponse + wantUpdatedProvider *idp_pb.Provider }{ { name: "no permissions, error", args: args{ ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), req: &admin_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Issuer: "zitadel.example.com", ClientId: "test-client", ClientSecret: "test-secret", @@ -278,7 +260,6 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeLogin), req: &admin_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Issuer: "acme.example.com", Scopes: []string{"email", "profile"}, ProviderOptions: &idp_pb.Options{ @@ -289,21 +270,11 @@ func Test_UpdateZitadelProvider(t *testing.T) { }, wantErr: status.Error(codes.PermissionDenied, "No matching permissions found (AUTH-5mWD2)"), }, - { - name: "missing required field: id", - args: args{ - ctx: AdminCTX, - req: &admin_pb.UpdateZitadelProviderRequest{}, - }, - wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.Id: value length must be between 1 and 200 runes, inclusive"), - }, { name: "missing required field: name", args: args{ ctx: AdminCTX, - req: &admin_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), - }, + req: &admin_pb.UpdateZitadelProviderRequest{}, }, wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.Name: value length must be between 1 and 200 runes, inclusive"), }, @@ -312,7 +283,6 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: AdminCTX, req: &admin_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Name: "Zitadel Support IdP updated", }, }, @@ -323,7 +293,6 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: AdminCTX, req: &admin_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Name: "Zitadel Support IdP updated", Issuer: "acme.example.com", }, @@ -335,7 +304,6 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: AdminCTX, req: &admin_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Name: "Zitadel Support IdP updated", Issuer: "acme.example.com", ClientId: "test-client", @@ -361,22 +329,54 @@ func Test_UpdateZitadelProvider(t *testing.T) { ResourceOwner: Instance.Instance.Id, }, }, + wantUpdatedProvider: &idp_pb.Provider{ + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: "Zitadel Support IdP updated", + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_SYSTEM, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile", "openid", "offline_access"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + { + OrganizationId: "org2", + OrganizationDomain: "org2.com", + }, + }, + }, + }, + }, + }, }, { name: "update with scopes unset, ok", args: args{ ctx: AdminCTX, req: &admin_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), - Name: "Zitadel Support IdP updated again", + Name: "Zitadel Support IdP updated 1", Issuer: "acme.example.com", ClientId: "test-client", - Scopes: nil, + Scopes: []string{}, ProviderOptions: &idp_pb.Options{ IsCreationAllowed: true, IsAutoCreation: false, }, - InstanceRolesInfo: nil, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org3", + OrganizationDomain: "org3.com", + }, + }, }, }, wantResponse: &admin_pb.UpdateZitadelProviderResponse{ @@ -384,12 +384,42 @@ func Test_UpdateZitadelProvider(t *testing.T) { ResourceOwner: Instance.Instance.Id, }, }, + wantUpdatedProvider: &idp_pb.Provider{ + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: "Zitadel Support IdP updated 1", + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_SYSTEM, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: nil, // unset scopes + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org3", + OrganizationDomain: "org3.com", + }, + }, + }, + }, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // create a new provider per subtest + existingProvider := Instance.AddZitadelProvider(AdminCTX, integration.IDPName()) + + // build request using this provider ID + tt.args.req.Id = existingProvider.GetId() + before := time.Now() - got, err := Client.UpdateZitadelProvider(tt.args.ctx, tt.args.req) + updateResp, err := Client.UpdateZitadelProvider(tt.args.ctx, tt.args.req) after := time.Now() if tt.wantErr != nil { require.Error(t, err) @@ -400,11 +430,292 @@ func Test_UpdateZitadelProvider(t *testing.T) { return } require.NoError(t, err) - assert.NotNil(t, got) - assert.WithinRange(t, got.GetDetails().GetChangeDate().AsTime(), before, after) - assert.Equal(t, tt.wantResponse.GetDetails().GetResourceOwner(), got.GetDetails().GetResourceOwner()) + assert.NotNil(t, updateResp) + assert.WithinRange(t, updateResp.GetDetails().GetChangeDate().AsTime(), before, after) + assert.Equal(t, tt.wantResponse.GetDetails().GetResourceOwner(), updateResp.GetDetails().GetResourceOwner()) - // todo: check if the update was actually applied by fetching the provider and checking its fields when the functionality is implemented + // get provider by ID and assert that the updated provider matches the expected values + getResp, err := Client.GetProviderByID(AdminCTX, &admin_pb.GetProviderByIDRequest{Id: existingProvider.GetId()}) + require.NoError(t, err) + assert.Equal(t, updateResp.GetDetails().GetChangeDate().AsTime(), getResp.GetIdp().GetDetails().GetChangeDate().AsTime()) + tt.wantUpdatedProvider.Id = existingProvider.GetId() + assertProvider(t, tt.wantUpdatedProvider, getResp.GetIdp()) }) } } + +func Test_UpdateZitadelProvider_MissingID(t *testing.T) { + _ = Instance.AddZitadelProvider(AdminCTX, integration.IDPName()) + // Attempt to update the provider without specifying the ID + updateResp, err := Client.UpdateZitadelProvider(AdminCTX, &admin_pb.UpdateZitadelProviderRequest{}) + require.Error(t, err) + grpcStatus, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, codes.InvalidArgument, grpcStatus.Code()) + assert.Equal(t, "invalid UpdateZitadelProviderRequest.Id: value length must be between 1 and 200 runes, inclusive", grpcStatus.Message()) + require.Nil(t, updateResp) +} + +func Test_GetProviderByID(t *testing.T) { + providerName := integration.IDPName() + existingProvider := Instance.AddZitadelProvider(AdminCTX, providerName) + + tests := []struct { + name string + ctx context.Context + req *admin_pb.GetProviderByIDRequest + wantErr error + wantResp *admin_pb.GetProviderByIDResponse + }{ + { + name: "no permissions", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &admin_pb.GetProviderByIDRequest{ + Id: "idp-id", + }, + wantErr: status.Error(codes.NotFound, "membership not found (AUTHZ-cdgFk)"), + }, + { + name: "invalid ID", + ctx: AdminCTX, + wantErr: status.Error(codes.InvalidArgument, "invalid GetProviderByIDRequest.Id: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "non-existing ID", + ctx: AdminCTX, + req: &admin_pb.GetProviderByIDRequest{ + Id: "non-existing-id", + }, + wantErr: status.Error(codes.NotFound, "Identity Provider Configuration doesn't exist (QUERY-SAFrt)"), + }, + { + name: "insufficient permissions", // no iam.idp.read permissions set + ctx: integration.WithSystemUserWithNoPermissionsAuthorization(CTX), + req: &admin_pb.GetProviderByIDRequest{ + Id: existingProvider.GetId(), + }, + wantErr: status.Error(codes.PermissionDenied, "No matching permissions found (AUTH-5mWD2)"), + }, + { + name: "found", + ctx: AdminCTX, + req: &admin_pb.GetProviderByIDRequest{ + Id: existingProvider.GetId(), + }, + wantResp: &admin_pb.GetProviderByIDResponse{ + Idp: &idp_pb.Provider{ + Id: existingProvider.GetId(), + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: providerName, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_SYSTEM, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GetProviderByID(tt.ctx, tt.req) + 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.Equal(t, existingProvider.GetDetails().GetCreationDate().AsTime(), got.GetIdp().GetDetails().GetCreationDate().AsTime()) + assert.Equal(t, existingProvider.GetDetails().GetResourceOwner(), got.GetIdp().GetDetails().GetResourceOwner()) + assertProvider(t, tt.wantResp.GetIdp(), got.GetIdp()) + }) + } +} + +func Test_ListProviders(t *testing.T) { + provider1Name := integration.IDPName() + provider1 := Instance.AddZitadelProvider(AdminCTX, provider1Name) + + provider2Name := integration.IDPName() + provider2 := Instance.AddZitadelProvider(AdminCTX, provider2Name) + + tests := []struct { + name string + ctx context.Context + req *admin_pb.ListProvidersRequest + wantResp *admin_pb.ListProvidersResponse + wantErr error + }{ + { + name: "no permissions", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &admin_pb.ListProvidersRequest{}, + wantErr: status.Error(codes.NotFound, "membership not found (AUTHZ-cdgFk)"), + }, + { + name: "insufficient permissions", // no iam.idp.read permissions set + ctx: integration.WithSystemUserWithNoPermissionsAuthorization(CTX), + req: &admin_pb.ListProvidersRequest{}, + wantErr: status.Error(codes.PermissionDenied, "No matching permissions found (AUTH-5mWD2)"), + }, + { + name: "list by id", + ctx: AdminCTX, + req: &admin_pb.ListProvidersRequest{ + Query: &object_pb.ListQuery{ + Asc: false, + }, + Queries: []*admin_pb.ProviderQuery{ + { + Query: &admin_pb.ProviderQuery_IdpIdQuery{ + IdpIdQuery: &idp_pb.IDPIDQuery{ + Id: provider2.GetId(), + }, + }, + }, + }, + }, + wantResp: &admin_pb.ListProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + }, + Result: []*idp_pb.Provider{ + { + Id: provider2.GetId(), + Details: &object_pb.ObjectDetails{ + CreationDate: provider2.GetDetails().GetCreationDate(), + ChangeDate: provider2.GetDetails().GetChangeDate(), + ResourceOwner: provider2.GetDetails().GetResourceOwner(), + }, + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: provider2Name, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_SYSTEM, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "list by name", + ctx: AdminCTX, + req: &admin_pb.ListProvidersRequest{ + Query: &object_pb.ListQuery{ + Asc: false, + }, + Queries: []*admin_pb.ProviderQuery{ + { + Query: &admin_pb.ProviderQuery_IdpNameQuery{ + IdpNameQuery: &idp_pb.IDPNameQuery{ + Name: provider1Name, + }, + }, + }, + }, + }, + wantResp: &admin_pb.ListProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + }, + Result: []*idp_pb.Provider{ + { + Id: provider1.GetId(), + Details: &object_pb.ObjectDetails{ + CreationDate: provider1.GetDetails().GetCreationDate(), + ChangeDate: provider1.GetDetails().GetChangeDate(), + ResourceOwner: provider1.GetDetails().GetResourceOwner(), + }, + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: provider1Name, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_SYSTEM, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListProviders(tt.ctx, tt.req) + 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.Equal(t, tt.wantResp.GetDetails().GetTotalResult(), got.GetDetails().GetTotalResult()) + for i, want := range tt.wantResp.GetResult() { + assert.Equal(t, want.GetDetails().GetCreationDate().AsTime(), got.GetResult()[i].GetDetails().GetCreationDate().AsTime()) + assert.Equal(t, Instance.ID(), got.GetResult()[i].GetDetails().GetResourceOwner()) + assertProvider(t, want, got.GetResult()[i]) + } + }) + } +} + +func assertProvider(t *testing.T, expected, actual *idp_pb.Provider) { + assert.Equal(t, expected.GetId(), actual.GetId()) + assert.Equal(t, expected.GetState(), actual.GetState()) + assert.Equal(t, expected.GetName(), actual.GetName()) + assert.Equal(t, expected.GetOwner(), actual.GetOwner()) + assert.Equal(t, expected.GetType(), actual.GetType()) + assert.True(t, proto.Equal(expected.GetConfig(), actual.GetConfig()), "expected: %v, actual: %v", expected.GetConfig(), actual.GetConfig()) +} diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index b88400922..c80b4d5c5 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -415,6 +415,8 @@ func providerTypeToPb(idpType domain.IDPType) idp_pb.ProviderType { return idp_pb.ProviderType_PROVIDER_TYPE_APPLE case domain.IDPTypeSAML: return idp_pb.ProviderType_PROVIDER_TYPE_SAML + case domain.IDPTypeZitadel: + return idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL case domain.IDPTypeUnspecified: return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED default: @@ -480,6 +482,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig { samlConfigToPb(providerConfig, config.SAMLIDPTemplate) return providerConfig } + if config.ZitadelIDPTemplate != nil { + zitadelConfigToPb(providerConfig, config.ZitadelIDPTemplate) + return providerConfig + } return providerConfig } @@ -677,6 +683,24 @@ func samlConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.SAMLI } } +func zitadelConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.ZitadelIDPTemplate) { + instanceRolesInfo := make([]*idp_pb.InstanceRolesInfo, 0, len(template.InstanceRolesInfo)) + for _, role := range template.InstanceRolesInfo { + instanceRolesInfo = append(instanceRolesInfo, &idp_pb.InstanceRolesInfo{ + OrganizationDomain: role.OrganizationDomain, + OrganizationId: role.OrganizationID, + }) + } + providerConfig.Config = &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + ClientId: template.ClientID, + Issuer: template.Issuer, + Scopes: template.Scopes, + InstanceRolesInfo: instanceRolesInfo, + }, + } +} + func bindingToPb(binding string) idp_pb.SAMLBinding { switch binding { case "": diff --git a/internal/api/grpc/idp/v2/integration_test/query_test.go b/internal/api/grpc/idp/v2/integration_test/query_test.go index bdbd49468..06e4f82d9 100644 --- a/internal/api/grpc/idp/v2/integration_test/query_test.go +++ b/internal/api/grpc/idp/v2/integration_test/query_test.go @@ -202,6 +202,102 @@ func TestServer_GetIDPByID(t *testing.T) { }, wantErr: true, }, + { + name: "idp by ID, instance, Zitadel provider, ok", + args: args{ + IamCTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := integration.IDPName() + resp := Instance.AddZitadelProvider(ctx, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + CreationDate: resp.Details.CreationDate, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + want: &idp.GetIDPByIDResponse{ + Idp: &idp.IDP{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + }, + State: idp.IDPState_IDP_STATE_ACTIVE, + Type: idp.IDPType_IDP_TYPE_ZITADEL, + Config: &idp.IDPConfig{ + Config: &idp.IDPConfig_Zitadel{ + Zitadel: &idp.ZitadelConfig{ + ClientId: "test-client", + Issuer: "zitadel.example.com", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + Options: &idp.Options{ + IsCreationAllowed: true, + }, + }, + }, + }, + }, + { + name: "idp by ID, org, Zitadel provider, ok", + args: args{ + CTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := integration.IDPName() + resp := Instance.AddOrgZitadelProvider(ctx, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + CreationDate: resp.Details.CreationDate, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + want: &idp.GetIDPByIDResponse{ + Idp: &idp.IDP{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + }, + State: idp.IDPState_IDP_STATE_ACTIVE, + Type: idp.IDPType_IDP_TYPE_ZITADEL, + Config: &idp.IDPConfig{ + Config: &idp.IDPConfig_Zitadel{ + Zitadel: &idp.ZitadelConfig{ + ClientId: "test-client", + Issuer: "zitadel.example.com", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + Options: &idp.Options{ + IsCreationAllowed: true, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/idp/v2/query.go b/internal/api/grpc/idp/v2/query.go index 8c9a892a7..46c4cc30c 100644 --- a/internal/api/grpc/idp/v2/query.go +++ b/internal/api/grpc/idp/v2/query.go @@ -85,6 +85,8 @@ func idpTypeToPb(idpType domain.IDPType) idp_pb.IDPType { return idp_pb.IDPType_IDP_TYPE_APPLE case domain.IDPTypeSAML: return idp_pb.IDPType_IDP_TYPE_SAML + case domain.IDPTypeZitadel: + return idp_pb.IDPType_IDP_TYPE_ZITADEL case domain.IDPTypeUnspecified: return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED default: @@ -150,6 +152,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.IDPConfig { samlConfigToPb(idpConfig, config.SAMLIDPTemplate) return idpConfig } + if config.ZitadelIDPTemplate != nil { + zitadelConfigToPb(idpConfig, config.ZitadelIDPTemplate) + return idpConfig + } return idpConfig } @@ -387,3 +393,21 @@ func signatureAlgorithmToPb(signatureAlgorithm string) idp_pb.SAMLSignatureAlgor return idp_pb.SAMLSignatureAlgorithm_SAML_SIGNATURE_UNSPECIFIED } } + +func zitadelConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.ZitadelIDPTemplate) { + instanceRolesInfo := make([]*idp_pb.InstanceRolesInfo, 0, len(template.InstanceRolesInfo)) + for _, role := range template.InstanceRolesInfo { + instanceRolesInfo = append(instanceRolesInfo, &idp_pb.InstanceRolesInfo{ + OrganizationId: role.OrganizationID, + OrganizationDomain: role.OrganizationDomain, + }) + } + idpConfig.Config = &idp_pb.IDPConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + ClientId: template.ClientID, + Issuer: template.Issuer, + Scopes: template.Scopes, + InstanceRolesInfo: instanceRolesInfo, + }, + } +} diff --git a/internal/api/grpc/management/integration_test/idp_test.go b/internal/api/grpc/management/integration_test/idp_test.go index fdf42ffd5..a64a3f995 100644 --- a/internal/api/grpc/management/integration_test/idp_test.go +++ b/internal/api/grpc/management/integration_test/idp_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/zitadel/zitadel/internal/integration" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" @@ -233,41 +234,22 @@ func Test_AddZitadelProvider(t *testing.T) { } func Test_UpdateZitadelProvider(t *testing.T) { - existingProvider, err := Instance.Client.Mgmt.AddZitadelProvider(OrgCTX, &mgmt_pb.AddZitadelProviderRequest{ - Name: "Zitadel Support IdP", - Issuer: "zitadel.example.com", - ClientId: "test-client", - ClientSecret: "test-secret", - Scopes: []string{"email", "profile"}, - ProviderOptions: &idp_pb.Options{ - IsCreationAllowed: true, - }, - InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ - { - OrganizationId: "org1", - OrganizationDomain: "org1.com", - }, - }, - }) - require.NoError(t, err) - require.NotEmpty(t, existingProvider.GetId()) - type args struct { ctx context.Context req *mgmt_pb.UpdateZitadelProviderRequest } tests := []struct { - name string - args args - wantErr error - wantResponse *mgmt_pb.UpdateZitadelProviderResponse + name string + args args + wantErr error + wantResponse *mgmt_pb.UpdateZitadelProviderResponse + wantUpdatedProvider *idp_pb.Provider }{ { name: "no permissions, error", args: args{ ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), req: &mgmt_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Issuer: "zitadel.example.com", ClientId: "test-client", ClientSecret: "test-secret", @@ -284,7 +266,6 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeLogin), req: &mgmt_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Issuer: "acme.example.com", Scopes: []string{"email", "profile"}, ProviderOptions: &idp_pb.Options{ @@ -295,21 +276,11 @@ func Test_UpdateZitadelProvider(t *testing.T) { }, wantErr: status.Error(codes.PermissionDenied, "No matching permissions found (AUTH-5mWD2)"), }, - { - name: "missing required field: id", - args: args{ - ctx: OrgCTX, - req: &mgmt_pb.UpdateZitadelProviderRequest{}, - }, - wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.Id: value length must be between 1 and 200 runes, inclusive"), - }, { name: "missing required field: name", args: args{ ctx: OrgCTX, - req: &mgmt_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), - }, + req: &mgmt_pb.UpdateZitadelProviderRequest{}, }, wantErr: status.Error(codes.InvalidArgument, "invalid UpdateZitadelProviderRequest.Name: value length must be between 1 and 200 runes, inclusive"), }, @@ -318,7 +289,6 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: OrgCTX, req: &mgmt_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Name: "Zitadel Support IdP updated", }, }, @@ -329,7 +299,6 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: OrgCTX, req: &mgmt_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Name: "Zitadel Support IdP updated", Issuer: "acme.example.com", }, @@ -341,15 +310,10 @@ func Test_UpdateZitadelProvider(t *testing.T) { args: args{ ctx: OrgCTX, req: &mgmt_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), Name: "Zitadel Support IdP updated", Issuer: "acme.example.com", ClientId: "test-client", Scopes: []string{"email", "profile", "openid", "offline_access"}, - ProviderOptions: &idp_pb.Options{ - IsCreationAllowed: true, - IsAutoCreation: false, - }, InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ { OrganizationId: "org1", @@ -367,22 +331,51 @@ func Test_UpdateZitadelProvider(t *testing.T) { ResourceOwner: Instance.DefaultOrg.Id, }, }, + wantUpdatedProvider: &idp_pb.Provider{ + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: "Zitadel Support IdP updated", + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{}, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile", "openid", "offline_access"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + { + OrganizationId: "org2", + OrganizationDomain: "org2.com", + }, + }, + }, + }, + }, + }, }, { name: "update with scopes unset, ok", args: args{ ctx: OrgCTX, req: &mgmt_pb.UpdateZitadelProviderRequest{ - Id: existingProvider.GetId(), - Name: "Zitadel Support IdP updated again", + Name: "Zitadel Support IdP updated 1", Issuer: "acme.example.com", ClientId: "test-client", - Scopes: []string{}, + Scopes: []string{}, // scopes unset -> will be updated by the API ProviderOptions: &idp_pb.Options{ - IsCreationAllowed: true, - IsAutoCreation: false, + IsAutoCreation: true, + }, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org3", + OrganizationDomain: "org3.com", + }, }, - InstanceRolesInfo: nil, }, }, wantResponse: &mgmt_pb.UpdateZitadelProviderResponse{ @@ -390,12 +383,80 @@ func Test_UpdateZitadelProvider(t *testing.T) { ResourceOwner: Instance.DefaultOrg.Id, }, }, + wantUpdatedProvider: &idp_pb.Provider{ + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: "Zitadel Support IdP updated 1", + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsAutoCreation: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: nil, // unset scopes + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org3", + OrganizationDomain: "org3.com", + }, + }, + }, + }, + }, + }, + }, + { + name: "update with instance roles info not set, ok", + args: args{ + ctx: OrgCTX, + req: &mgmt_pb.UpdateZitadelProviderRequest{ + Name: "Zitadel Support IdP updated 2", + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: []string{"email", "openid"}, + ProviderOptions: &idp_pb.Options{ + IsCreationAllowed: true, + }, + InstanceRolesInfo: nil, // instance roles info isn't set -> will be unset by the API + }, + }, + wantResponse: &mgmt_pb.UpdateZitadelProviderResponse{ + Details: &object_pb.ObjectDetails{ + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + wantUpdatedProvider: &idp_pb.Provider{ + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: "Zitadel Support IdP updated 2", + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "acme.example.com", + ClientId: "test-client", + Scopes: []string{"email", "openid"}, + InstanceRolesInfo: nil, // instance roles info isn't set + }, + }, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // create a new provider per subtest and set the ID in the request + zitadelProvider := Instance.AddOrgZitadelProvider(OrgCTX, integration.IDPName()) + tt.args.req.Id = zitadelProvider.Id + before := time.Now() - got, err := Client.UpdateZitadelProvider(tt.args.ctx, tt.args.req) + updateResp, err := Client.UpdateZitadelProvider(tt.args.ctx, tt.args.req) after := time.Now() if tt.wantErr != nil { require.Error(t, err) @@ -406,11 +467,371 @@ func Test_UpdateZitadelProvider(t *testing.T) { return } require.NoError(t, err) - assert.NotNil(t, got) - assert.WithinRange(t, got.GetDetails().GetChangeDate().AsTime(), before, after) - assert.Equal(t, tt.wantResponse.GetDetails().GetResourceOwner(), got.GetDetails().GetResourceOwner()) + assert.NotNil(t, updateResp) + assert.WithinRange(t, updateResp.GetDetails().GetChangeDate().AsTime(), before, after) + assert.Equal(t, tt.wantResponse.GetDetails().GetResourceOwner(), updateResp.GetDetails().GetResourceOwner()) - // todo: check if the update was actually applied by fetching the provider and checking its fields when the functionality is implemented + // get provider by ID and assert that the updated provider matches the expected values + getResp, err := Client.GetProviderByID(OrgCTX, &mgmt_pb.GetProviderByIDRequest{Id: zitadelProvider.Id}) + require.NoError(t, err) + assert.Equal(t, updateResp.GetDetails().GetChangeDate().AsTime(), getResp.GetIdp().GetDetails().GetChangeDate().AsTime()) + tt.wantUpdatedProvider.Id = zitadelProvider.Id + assertProvider(t, tt.wantUpdatedProvider, getResp.GetIdp()) }) } } + +func Test_UpdateZitadelProvider_MissingID(t *testing.T) { + _ = Instance.AddOrgZitadelProvider(OrgCTX, integration.IDPName()) + // Attempt to update the provider without specifying the ID + updateResp, err := Client.UpdateZitadelProvider(OrgCTX, &mgmt_pb.UpdateZitadelProviderRequest{}) + require.Error(t, err) + grpcStatus, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, codes.InvalidArgument, grpcStatus.Code()) + assert.Equal(t, "invalid UpdateZitadelProviderRequest.Id: value length must be between 1 and 200 runes, inclusive", grpcStatus.Message()) + require.Nil(t, updateResp) +} + +func Test_GetProviderByID(t *testing.T) { + name := integration.IDPName() + existingProvider := Instance.AddOrgZitadelProvider(OrgCTX, name) + + tests := []struct { + name string + ctx context.Context + req *mgmt_pb.GetProviderByIDRequest + wantErr error + wantResp *mgmt_pb.GetProviderByIDResponse + }{ + { + name: "no permissions", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &mgmt_pb.GetProviderByIDRequest{ + Id: "idp-id", + }, + wantErr: status.Error(codes.NotFound, "membership not found (AUTHZ-cdgFk)"), + }, + { + name: "invalid ID", + ctx: OrgCTX, + wantErr: status.Error(codes.InvalidArgument, "invalid GetProviderByIDRequest.Id: value length must be between 1 and 200 runes, inclusive"), + }, + { + name: "non-existing ID", + ctx: OrgCTX, + req: &mgmt_pb.GetProviderByIDRequest{ + Id: "non-existing-id", + }, + wantErr: status.Error(codes.NotFound, "Identity Provider Configuration doesn't exist (QUERY-SAFrt)"), + }, + { + name: "insufficient permissions", // no org.idp.read permissions set + ctx: integration.WithSystemUserWithNoPermissionsAuthorization(CTX), + req: &mgmt_pb.GetProviderByIDRequest{ + Id: existingProvider.GetId(), + }, + wantErr: status.Error(codes.PermissionDenied, "No matching permissions found (AUTH-5mWD2)"), + }, + { + name: "found", + ctx: OrgCTX, + req: &mgmt_pb.GetProviderByIDRequest{ + Id: existingProvider.GetId(), + }, + wantResp: &mgmt_pb.GetProviderByIDResponse{ + Idp: &idp_pb.Provider{ + Id: existingProvider.GetId(), + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: name, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GetProviderByID(tt.ctx, tt.req) + 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.Equal(t, existingProvider.GetDetails().GetCreationDate().AsTime(), got.GetIdp().GetDetails().GetCreationDate().AsTime()) + assert.Equal(t, existingProvider.GetDetails().GetResourceOwner(), got.GetIdp().GetDetails().GetResourceOwner()) + assertProvider(t, tt.wantResp.GetIdp(), got.GetIdp()) + }) + } +} + +func Test_ListProviders(t *testing.T) { + org := Instance.CreateOrganization(IAMOwnerCTX, integration.OrganizationName(), integration.Email()) + require.NotEmpty(t, org.GetOrganizationId()) + orgCtx := integration.SetOrgID(IAMOwnerCTX, org.GetOrganizationId()) + + provider1Name := integration.IDPName() + provider1 := Instance.AddOrgZitadelProvider(orgCtx, provider1Name) + provider2Name := integration.IDPName() + provider2 := Instance.AddOrgZitadelProvider(orgCtx, provider2Name) + + tests := []struct { + name string + ctx context.Context + req *mgmt_pb.ListProvidersRequest + wantResp *mgmt_pb.ListProvidersResponse + wantErr error + }{ + { + name: "no permissions", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &mgmt_pb.ListProvidersRequest{}, + wantErr: status.Error(codes.NotFound, "membership not found (AUTHZ-cdgFk)"), + }, + { + name: "insufficient permissions", // no iam.idp.read permissions set + ctx: integration.WithSystemUserWithNoPermissionsAuthorization(CTX), + req: &mgmt_pb.ListProvidersRequest{}, + wantErr: status.Error(codes.PermissionDenied, "No matching permissions found (AUTH-5mWD2)"), + }, + { + name: "list by id", + ctx: orgCtx, + req: &mgmt_pb.ListProvidersRequest{ + Query: &object_pb.ListQuery{ + Asc: false, + }, + Queries: []*mgmt_pb.ProviderQuery{ + { + Query: &mgmt_pb.ProviderQuery_IdpIdQuery{ + IdpIdQuery: &idp_pb.IDPIDQuery{ + Id: provider2.GetId(), + }, + }, + }, + }, + }, + wantResp: &mgmt_pb.ListProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + }, + Result: []*idp_pb.Provider{ + { + Id: provider2.GetId(), + Details: &object_pb.ObjectDetails{ + CreationDate: provider2.GetDetails().GetCreationDate(), + ChangeDate: provider2.GetDetails().GetChangeDate(), + ResourceOwner: provider2.GetDetails().GetResourceOwner(), + }, + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: provider2Name, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "list by name", + ctx: orgCtx, + req: &mgmt_pb.ListProvidersRequest{ + Query: &object_pb.ListQuery{ + Asc: false, + }, + Queries: []*mgmt_pb.ProviderQuery{ + { + Query: &mgmt_pb.ProviderQuery_IdpNameQuery{ + IdpNameQuery: &idp_pb.IDPNameQuery{ + Name: provider1Name, + }, + }, + }, + }, + }, + wantResp: &mgmt_pb.ListProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + }, + Result: []*idp_pb.Provider{ + { + Id: provider1.GetId(), + Details: &object_pb.ObjectDetails{ + CreationDate: provider1.GetDetails().GetCreationDate(), + ChangeDate: provider1.GetDetails().GetChangeDate(), + ResourceOwner: provider1.GetDetails().GetResourceOwner(), + }, + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: provider1Name, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "list all providers", + ctx: orgCtx, + req: &mgmt_pb.ListProvidersRequest{ + Query: &object_pb.ListQuery{ + Asc: true, + }, + }, + wantResp: &mgmt_pb.ListProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 2, + }, + Result: []*idp_pb.Provider{ + { + Id: provider1.GetId(), + Details: &object_pb.ObjectDetails{ + CreationDate: provider1.GetDetails().GetCreationDate(), + ChangeDate: provider1.GetDetails().GetChangeDate(), + ResourceOwner: provider1.GetDetails().GetResourceOwner(), + }, + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: provider1Name, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + { + Id: provider2.GetId(), + Details: &object_pb.ObjectDetails{ + CreationDate: provider2.GetDetails().GetCreationDate(), + ChangeDate: provider2.GetDetails().GetChangeDate(), + ResourceOwner: provider2.GetDetails().GetResourceOwner(), + }, + State: idp_pb.IDPState_IDP_STATE_ACTIVE, + Name: provider2Name, + Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG, + Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL, + Config: &idp_pb.ProviderConfig{ + Options: &idp_pb.Options{ + IsCreationAllowed: true, + }, + Config: &idp_pb.ProviderConfig_Zitadel{ + Zitadel: &idp_pb.ZitadelConfig{ + Issuer: "zitadel.example.com", + ClientId: "test-client", + Scopes: []string{"email", "profile"}, + InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListProviders(tt.ctx, tt.req) + 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.Equal(t, tt.wantResp.GetDetails().GetTotalResult(), got.GetDetails().GetTotalResult()) + for i, want := range tt.wantResp.GetResult() { + assert.Equal(t, want.GetDetails().GetCreationDate().AsTime(), got.GetResult()[i].GetDetails().GetCreationDate().AsTime()) + assert.Equal(t, org.GetOrganizationId(), got.GetResult()[i].GetDetails().GetResourceOwner()) + assertProvider(t, want, got.GetResult()[i]) + } + }) + } +} + +func assertProvider(t *testing.T, expected, actual *idp_pb.Provider) { + assert.Equal(t, expected.GetId(), actual.GetId()) + assert.Equal(t, expected.GetState(), actual.GetState()) + assert.Equal(t, expected.GetName(), actual.GetName()) + assert.Equal(t, expected.GetOwner(), actual.GetOwner()) + assert.Equal(t, expected.GetType(), actual.GetType()) + assert.True(t, proto.Equal(expected.GetConfig(), actual.GetConfig()), "expected: %v, actual: %v", expected.GetConfig(), actual.GetConfig()) +} diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 0998fe3b0..9ba93b7fd 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -2461,10 +2461,10 @@ func (wm *ZitadelIDPWriteModel) NewChanges( if wm.Issuer != issuer { changes = append(changes, idp.ChangeZitadelIDPIssuer(issuer)) } - if scopes != nil && !slices.Equal(wm.Scopes, scopes) { + if !slices.Equal(wm.Scopes, scopes) { changes = append(changes, idp.ChangeZitadelIDPScopes(scopes)) } - if info != nil && !slices.Equal(wm.InstanceRolesInfo, info) { + if !slices.Equal(wm.InstanceRolesInfo, info) { changes = append(changes, idp.ChangeZitadelIDPInstanceRolesInfo(info)) } opts := wm.Changes(options) diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 61c011631..883575b3b 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -6424,6 +6424,8 @@ func TestCommands_UpdateInstanceZitadelProvider(t *testing.T) { KeyID: "id", Crypted: []byte("newSecret"), }), + idp.ChangeZitadelIDPInstanceRolesInfo(nil), + idp.ChangeZitadelIDPScopes(nil), }, ), ), @@ -6489,6 +6491,17 @@ func TestCommands_UpdateInstanceZitadelProvider(t *testing.T) { Name: "name", Issuer: "issuer", ClientID: "clientID", + Scopes: []string{openid.ScopeOpenID, openid.ScopeEmail}, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, }, }, res: res{ @@ -6756,7 +6769,7 @@ func TestCommands_UpdateInstanceZitadelProvider(t *testing.T) { Issuer: "new issuer", ClientID: "clientID2", ClientSecret: "newSecret", - Scopes: nil, + Scopes: []string{openid.ScopeOpenID, openid.ScopeEmail}, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 090d6868c..436939616 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -6459,6 +6459,8 @@ func TestCommands_UpdateOrgZitadelIDP(t *testing.T) { KeyID: "id", Crypted: []byte("newSecret"), }), + idp.ChangeZitadelIDPScopes(nil), + idp.ChangeZitadelIDPInstanceRolesInfo(nil), }, ), ), @@ -6524,6 +6526,17 @@ func TestCommands_UpdateOrgZitadelIDP(t *testing.T) { Name: "name", Issuer: "issuer", ClientID: "clientID", + Scopes: []string{openid.ScopeOpenID, openid.ScopeEmail}, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "example-org1.com", + }, + { + OrganizationID: "org2", + OrganizationDomain: "example-org2.com", + }, + }, }, }, res: res{ @@ -6791,7 +6804,7 @@ func TestCommands_UpdateOrgZitadelIDP(t *testing.T) { Issuer: "new issuer", ClientID: "clientID2", ClientSecret: "newSecret", - Scopes: nil, + Scopes: []string{openid.ScopeOpenID, openid.ScopeEmail}, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, diff --git a/internal/integration/client.go b/internal/integration/client.go index 0debd8bd2..943ea2772 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -752,6 +752,48 @@ func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) return resp } +func (i *Instance) AddZitadelProvider(ctx context.Context, name string) *admin.AddZitadelProviderResponse { + resp, err := i.Client.Admin.AddZitadelProvider(ctx, &admin.AddZitadelProviderRequest{ + Name: name, + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp.Options{ + IsCreationAllowed: true, + }, + InstanceRolesInfo: []*idp.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }) + logging.OnError(err).Panic("create zitadel idp") + return resp +} + +func (i *Instance) AddOrgZitadelProvider(ctx context.Context, name string) *mgmt.AddZitadelProviderResponse { + resp, err := i.Client.Mgmt.AddZitadelProvider(ctx, &mgmt.AddZitadelProviderRequest{ + Name: name, + Issuer: "zitadel.example.com", + ClientId: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"email", "profile"}, + ProviderOptions: &idp.Options{ + IsCreationAllowed: true, + }, + InstanceRolesInfo: []*idp.InstanceRolesInfo{ + { + OrganizationId: "org1", + OrganizationDomain: "org1.com", + }, + }, + }) + logging.OnError(err).Panic("create org zitadel idp") + return resp +} + func (i *Instance) AddGenericOIDCProvider(ctx context.Context, name string) *admin.AddGenericOIDCProviderResponse { resp, err := i.Client.Admin.AddGenericOIDCProvider(ctx, &admin.AddGenericOIDCProviderRequest{ Name: name, diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 35b8c18c7..6dd88c185 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -47,6 +47,7 @@ type IDPTemplate struct { *LDAPIDPTemplate *AppleIDPTemplate *SAMLIDPTemplate + *ZitadelIDPTemplate } type IDPTemplates struct { @@ -170,6 +171,15 @@ type SAMLIDPTemplate struct { FederatedLogoutEnabled bool } +type ZitadelIDPTemplate struct { + IDPID string + ClientID string + ClientSecret *crypto.CryptoValue + Issuer string + Scopes database.TextArray[string] + InstanceRolesInfo database.JSONArray[idp.RolesInfo] +} + var ( idpTemplateTable = table{ name: projection.IDPTemplateTable, @@ -741,6 +751,41 @@ var ( } ) +var ( + zitadelIdpTemplateTable = table{ + name: projection.IDPTemplateZitadelTable, + instanceIDCol: projection.ZitadelInstanceIDCol, + } + ZitadelIDCol = Column{ + name: projection.ZitadelIDCol, + table: zitadelIdpTemplateTable, + } + ZitadelInstanceIDCol = Column{ + name: projection.ZitadelInstanceIDCol, + table: zitadelIdpTemplateTable, + } + ZitadelIssuerCol = Column{ + name: projection.ZitadelIssuerCol, + table: zitadelIdpTemplateTable, + } + ZitadelClientIDCol = Column{ + name: projection.ZitadelClientIDCol, + table: zitadelIdpTemplateTable, + } + ZitadelClientSecretCol = Column{ + name: projection.ZitadelClientSecretCol, + table: zitadelIdpTemplateTable, + } + ZitadelScopesCol = Column{ + name: projection.ZitadelScopesCol, + table: zitadelIdpTemplateTable, + } + ZitadelInstanceRolesInfoCol = Column{ + name: projection.ZitadelInstanceRolesInfoCol, + table: zitadelIdpTemplateTable, + } +) + // IDPTemplateByID searches for the requested id with permission check if necessary func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, permissionCheck domain.PermissionCheck, queries ...SearchQuery) (template *IDPTemplate, err error) { idp, err := q.idpTemplateByID(ctx, shouldTriggerBulk, id, withOwnerRemoved, queries...) @@ -998,6 +1043,13 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla AppleKeyIDCol.identifier(), ApplePrivateKeyCol.identifier(), AppleScopesCol.identifier(), + // zitadel + ZitadelIDCol.identifier(), + ZitadelIssuerCol.identifier(), + ZitadelClientIDCol.identifier(), + ZitadelClientSecretCol.identifier(), + ZitadelScopesCol.identifier(), + ZitadelInstanceRolesInfoCol.identifier(), ).From(idpTemplateTable.identifier()). LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)). LeftJoin(join(OIDCIDCol, IDPTemplateIDCol)). @@ -1011,6 +1063,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). LeftJoin(join(AppleIDCol, IDPTemplateIDCol)). + LeftJoin(join(ZitadelIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IDPTemplate, error) { idpTemplate := new(IDPTemplate) @@ -1121,6 +1174,13 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla applePrivateKey := new(crypto.CryptoValue) appleScopes := database.TextArray[string]{} + zitadelID := sql.NullString{} + zitadelClientID := sql.NullString{} + zitadelClientSecret := new(crypto.CryptoValue) + zitadelIssuer := sql.NullString{} + zitadelScopes := database.TextArray[string]{} + zitadelInstanceRolesInfo := database.JSONArray[idp.RolesInfo]{} + err := row.Scan( &idpTemplate.ID, &idpTemplate.ResourceOwner, @@ -1240,6 +1300,13 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla &appleKeyID, &applePrivateKey, &appleScopes, + // zitadel + &zitadelID, + &zitadelIssuer, + &zitadelClientID, + &zitadelClientSecret, + &zitadelScopes, + &zitadelInstanceRolesInfo, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -1392,7 +1459,16 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla Scopes: appleScopes, } } - + if zitadelID.Valid { + idpTemplate.ZitadelIDPTemplate = &ZitadelIDPTemplate{ + IDPID: zitadelID.String, + ClientID: zitadelClientID.String, + ClientSecret: zitadelClientSecret, + Issuer: zitadelIssuer.String, + Scopes: zitadelScopes, + InstanceRolesInfo: zitadelInstanceRolesInfo, + } + } return idpTemplate, nil } } @@ -1518,6 +1594,13 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate AppleKeyIDCol.identifier(), ApplePrivateKeyCol.identifier(), AppleScopesCol.identifier(), + // zitadel + ZitadelIDCol.identifier(), + ZitadelIssuerCol.identifier(), + ZitadelClientIDCol.identifier(), + ZitadelClientSecretCol.identifier(), + ZitadelScopesCol.identifier(), + ZitadelInstanceRolesInfoCol.identifier(), // count countColumn.identifier(), ).From(idpTemplateTable.identifier()). @@ -1533,6 +1616,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). LeftJoin(join(AppleIDCol, IDPTemplateIDCol)). + LeftJoin(join(ZitadelIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPTemplates, error) { templates := make([]*IDPTemplate, 0) @@ -1646,6 +1730,13 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate applePrivateKey := new(crypto.CryptoValue) appleScopes := database.TextArray[string]{} + zitadelID := sql.NullString{} + zitadelIssuer := sql.NullString{} + zitadelClientID := sql.NullString{} + zitadelClientSecret := new(crypto.CryptoValue) + zitadelScopes := database.TextArray[string]{} + zitadelInstanceRolesInfo := database.JSONArray[idp.RolesInfo]{} + err := rows.Scan( &idpTemplate.ID, &idpTemplate.ResourceOwner, @@ -1765,6 +1856,13 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate &appleKeyID, &applePrivateKey, &appleScopes, + // zitadel + &zitadelID, + &zitadelIssuer, + &zitadelClientID, + &zitadelClientSecret, + &zitadelScopes, + &zitadelInstanceRolesInfo, &count, ) @@ -1916,6 +2014,16 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate Scopes: appleScopes, } } + if zitadelID.Valid { + idpTemplate.ZitadelIDPTemplate = &ZitadelIDPTemplate{ + IDPID: zitadelID.String, + Issuer: zitadelIssuer.String, + ClientID: zitadelClientID.String, + ClientSecret: zitadelClientSecret, + Scopes: zitadelScopes, + InstanceRolesInfo: zitadelInstanceRolesInfo, + } + } templates = append(templates, idpTemplate) } diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index cc16ab8f5..7b6806119 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -133,7 +133,14 @@ var ( ` projections.idp_templates6_apple.team_id,` + ` projections.idp_templates6_apple.key_id,` + ` projections.idp_templates6_apple.private_key,` + - ` projections.idp_templates6_apple.scopes` + + ` projections.idp_templates6_apple.scopes,` + + // zitadel + ` projections.idp_templates6_zitadel.idp_id,` + + ` projections.idp_templates6_zitadel.issuer,` + + ` projections.idp_templates6_zitadel.client_id,` + + ` projections.idp_templates6_zitadel.client_secret,` + + ` projections.idp_templates6_zitadel.scopes,` + + ` projections.idp_templates6_zitadel.instance_roles_info` + ` FROM projections.idp_templates6` + ` LEFT JOIN projections.idp_templates6_oauth2 ON projections.idp_templates6.id = projections.idp_templates6_oauth2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_oauth2.instance_id` + ` LEFT JOIN projections.idp_templates6_oidc ON projections.idp_templates6.id = projections.idp_templates6_oidc.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_oidc.instance_id` + @@ -146,7 +153,8 @@ var ( ` LEFT JOIN projections.idp_templates6_google ON projections.idp_templates6.id = projections.idp_templates6_google.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_google.instance_id` + ` LEFT JOIN projections.idp_templates6_saml ON projections.idp_templates6.id = projections.idp_templates6_saml.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_saml.instance_id` + ` LEFT JOIN projections.idp_templates6_ldap2 ON projections.idp_templates6.id = projections.idp_templates6_ldap2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_ldap2.instance_id` + - ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + + ` LEFT JOIN projections.idp_templates6_zitadel ON projections.idp_templates6.id = projections.idp_templates6_zitadel.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_zitadel.instance_id` idpTemplateCols = []string{ "id", "resource_owner", @@ -266,6 +274,13 @@ var ( "key_id", "private_key", "scopes", + // zitadel config + "idp_id", + "issuer", + "client_id", + "client_secret", + "scopes", + "instance_roles_info", } idpTemplatesQuery = `SELECT projections.idp_templates6.id,` + ` projections.idp_templates6.resource_owner,` + @@ -385,6 +400,13 @@ var ( ` projections.idp_templates6_apple.key_id,` + ` projections.idp_templates6_apple.private_key,` + ` projections.idp_templates6_apple.scopes,` + + // zitadel + ` projections.idp_templates6_zitadel.idp_id,` + + ` projections.idp_templates6_zitadel.issuer,` + + ` projections.idp_templates6_zitadel.client_id,` + + ` projections.idp_templates6_zitadel.client_secret,` + + ` projections.idp_templates6_zitadel.scopes,` + + ` projections.idp_templates6_zitadel.instance_roles_info,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_templates6` + ` LEFT JOIN projections.idp_templates6_oauth2 ON projections.idp_templates6.id = projections.idp_templates6_oauth2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_oauth2.instance_id` + @@ -398,7 +420,8 @@ var ( ` LEFT JOIN projections.idp_templates6_google ON projections.idp_templates6.id = projections.idp_templates6_google.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_google.instance_id` + ` LEFT JOIN projections.idp_templates6_saml ON projections.idp_templates6.id = projections.idp_templates6_saml.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_saml.instance_id` + ` LEFT JOIN projections.idp_templates6_ldap2 ON projections.idp_templates6.id = projections.idp_templates6_ldap2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_ldap2.instance_id` + - ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + + ` LEFT JOIN projections.idp_templates6_zitadel ON projections.idp_templates6.id = projections.idp_templates6_zitadel.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_zitadel.instance_id` idpTemplatesCols = []string{ "id", "resource_owner", @@ -518,6 +541,14 @@ var ( "key_id", "private_key", "scopes", + // zitadel config + "idp_id", + "issuer", + "client_id", + "client_secret", + "scopes", + "instance_roles_info", + // count "count", } ) @@ -677,6 +708,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -834,6 +872,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -989,6 +1034,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -1143,6 +1195,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -1295,6 +1354,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -1447,6 +1513,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -1600,6 +1673,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -1752,6 +1832,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -1910,6 +1997,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -2083,6 +2177,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "key_id", nil, database.TextArray[string]{"profile"}, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -2111,6 +2212,172 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { }, }, }, + { + name: "prepareIDPTemplateByIDQuery zitadel idp", + prepare: prepareIDPTemplateByIDQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(idpTemplateQuery), + idpTemplateCols, + []driver.Value{ + "idp-id", + "ro", + testNow, + testNow, + uint64(20211109), + domain.IDPConfigStateActive, + "idp-name", + domain.IDPTypeZitadel, + domain.IdentityProviderTypeOrg, + true, + true, + true, + true, + domain.AutoLinkingOptionUsername, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // oidc + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // jwt + nil, + nil, + nil, + nil, + nil, + nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, + // github + nil, + nil, + nil, + nil, + // github enterprise + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // gitlab + nil, + nil, + nil, + nil, + // gitlab self hosted + nil, + nil, + nil, + nil, + nil, + // google + nil, + nil, + nil, + nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // ldap config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // apple + nil, + nil, + nil, + nil, + nil, + nil, + // zitadel + "idp-id", + "issuer", + "client_id", + nil, + database.TextArray[string]{"profile"}, + []byte(`[{"organizationId":"org1","organizationDomain":"org1.com"}]`), + }, + ), + }, + object: &IDPTemplate{ + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + ResourceOwner: "ro", + ID: "idp-id", + State: domain.IDPStateActive, + Name: "idp-name", + Type: domain.IDPTypeZitadel, + OwnerType: domain.IdentityProviderTypeOrg, + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: domain.AutoLinkingOptionUsername, + ZitadelIDPTemplate: &ZitadelIDPTemplate{ + IDPID: "idp-id", + Issuer: "issuer", + ClientID: "client_id", + ClientSecret: nil, + Scopes: []string{"profile"}, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, + }, { name: "prepareIDPTemplateByIDQuery no config", prepare: prepareIDPTemplateByIDQuery, @@ -2237,6 +2504,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, ), }, @@ -2419,6 +2693,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, }, ), @@ -2601,6 +2882,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, }, ), @@ -2756,6 +3044,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, { "idp-id-saml", @@ -2876,6 +3171,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, { "idp-id-google", @@ -2996,6 +3298,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, { "idp-id-oauth", @@ -3116,6 +3425,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, { "idp-id-oidc", @@ -3236,6 +3552,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, }, { "idp-id-jwt", @@ -3356,13 +3679,147 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // zitadel + nil, + nil, + nil, + nil, + nil, + nil, + }, + { + "idp-id-zitadel", + "ro", + testNow, + testNow, + uint64(20211109), + domain.IDPConfigStateActive, + "idp-name", + domain.IDPTypeZitadel, + domain.IdentityProviderTypeOrg, + true, + true, + true, + true, + domain.AutoLinkingOptionUsername, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // oidc + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // jwt + nil, + nil, + nil, + nil, + nil, + nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, + // github + nil, + nil, + nil, + nil, + // github enterprise + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // gitlab + nil, + nil, + nil, + nil, + // gitlab self hosted + nil, + nil, + nil, + nil, + nil, + // google + nil, + nil, + nil, + nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // ldap config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // apple + nil, + nil, + nil, + nil, + nil, + nil, + // zitadel + "idp-id-zitadel", + "issuer", + "client_id", + nil, + database.TextArray[string]{"profile"}, + []byte(`[{"organizationId":"org1","organizationDomain":"org1.com"}]`), }, }, ), }, object: &IDPTemplates{ SearchResponse: SearchResponse{ - Count: 6, + Count: 7, }, Templates: []*IDPTemplate{ { @@ -3458,7 +3915,6 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { Scopes: []string{"profile"}, }, }, - { CreationDate: testNow, ChangeDate: testNow, @@ -3535,6 +3991,35 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { Audience: "audience", }, }, + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + ResourceOwner: "ro", + ID: "idp-id-zitadel", + State: domain.IDPStateActive, + Name: "idp-name", + Type: domain.IDPTypeZitadel, + OwnerType: domain.IdentityProviderTypeOrg, + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: domain.AutoLinkingOptionUsername, + ZitadelIDPTemplate: &ZitadelIDPTemplate{ + IDPID: "idp-id-zitadel", + Issuer: "issuer", + ClientID: "client_id", + ClientSecret: nil, + Scopes: []string{"profile"}, + InstanceRolesInfo: []idp.RolesInfo{ + { + OrganizationID: "org1", + OrganizationDomain: "org1.com", + }, + }, + }, + }, }, }, }, diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 0c0fc191a..10017ce34 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -274,6 +274,7 @@ enum ProviderType { PROVIDER_TYPE_GOOGLE = 10; PROVIDER_TYPE_APPLE = 11; PROVIDER_TYPE_SAML = 12; + PROVIDER_TYPE_ZITADEL = 13; } enum SAMLBinding { @@ -312,6 +313,7 @@ message ProviderConfig { AzureADConfig azure_ad = 11; AppleConfig apple = 12; SAMLConfig saml = 13; + ZitadelConfig zitadel = 14; } } @@ -525,6 +527,32 @@ message AzureADConfig { ]; } +message ZitadelConfig { + string issuer = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://zitadel.example.com/\""; + description: "the OIDC issuer of the identity provider"; + } + ]; + string client_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + description: "client id generated by the identity provider"; + } + ]; + repeated string scopes = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + description: "the scopes requested by Zitadel during the request to the identity provider"; + } + ]; + repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the information about the organization and domain of the instance roles"; + } + ]; +} + message Options { bool is_linking_allowed = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto index 1411fd447..879d784b0 100644 --- a/proto/zitadel/idp/v2/idp.proto +++ b/proto/zitadel/idp/v2/idp.proto @@ -55,6 +55,7 @@ enum IDPType { IDP_TYPE_GOOGLE = 10; IDP_TYPE_APPLE = 11; IDP_TYPE_SAML = 12; + IDP_TYPE_ZITADEL = 13; } enum SAMLBinding { @@ -93,6 +94,7 @@ message IDPConfig { AzureADConfig azure_ad = 11; AppleConfig apple = 12; SAMLConfig saml = 13; + ZitadelConfig zitadel = 14; } } @@ -439,3 +441,49 @@ message AppleConfig { "[\"name\", \"email\"]"; } ]; } + +message InstanceRolesInfo { + string organization_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"69629023906488334\""; + description: + "the id of the organization"; + } ]; + string organization_domain = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"example-org.com\""; + description: + "the domain of the organization"; + } ]; +} + +message ZitadelConfig { + string issuer = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"https://zitadel.example.com/\""; + description: + "the OIDC issuer of the identity provider"; + } ]; + string client_id = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + description: + "client id generated by the identity provider"; + } ]; + repeated string scopes = 3 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + description: + "the scopes requested by Zitadel during the request to the identity provider"; + } ]; + repeated InstanceRolesInfo instance_roles_info = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: + "the information about the organization and domain of the instance roles"; + } ]; +}