diff --git a/cmd/start/start.go b/cmd/start/start.go index f944ef0327..0ecff76a9b 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -38,6 +38,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/auth" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta" + idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2" "github.com/zitadel/zitadel/internal/api/grpc/management" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" @@ -437,6 +438,9 @@ func startAPIs( if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index b65a53d20b..3aad64d9ef 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -356,6 +356,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + idp_v2: { + specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json", + outputDir: "docs/apis/resources/idp_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, }, }, ], diff --git a/docs/sidebars.js b/docs/sidebars.js index 49107a380c..98667395f1 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -673,12 +673,24 @@ module.exports = { link: { type: "generated-index", title: "Feature Service API", - slug: "/apis/resources/feature_service/v2", + slug: "/apis/resources/feature_service_v2", description: 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n' }, items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"), }, + { + type: "category", + label: "Identity Provider Lifecycle", + link: { + type: "generated-index", + title: "Identity Provider Service API", + slug: "/apis/resources/idp_service_v2", + description: + 'This API is intended to manage identity providers (IdPs) for ZITADEL.\n' + }, + items: require("./docs/apis/resources/idp_service_v2/sidebar.ts"), + }, ], }, { diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index 8182ad63b9..5528a94dbb 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -157,7 +157,7 @@ func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderB if err != nil { return nil, err } - idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, instanceIDQuery) + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, nil, instanceIDQuery) if err != nil { return nil, err } diff --git a/internal/api/grpc/idp/v2/query.go b/internal/api/grpc/idp/v2/query.go new file mode 100644 index 0000000000..476ac3dcdd --- /dev/null +++ b/internal/api/grpc/idp/v2/query.go @@ -0,0 +1,369 @@ +package idp + +import ( + "context" + + "github.com/crewjam/saml" + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp/providers/azuread" + "github.com/zitadel/zitadel/internal/query" + idp_rp "github.com/zitadel/zitadel/internal/repository/idp" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" +) + +func (s *Server) GetIDPByID(ctx context.Context, req *idp_pb.GetIDPByIDRequest) (*idp_pb.GetIDPByIDResponse, error) { + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, s.checkPermission) + if err != nil { + return nil, err + } + return &idp_pb.GetIDPByIDResponse{Idp: idpToPb(idp)}, nil +} + +func idpToPb(idp *query.IDPTemplate) *idp_pb.IDP { + return &idp_pb.IDP{ + Id: idp.ID, + Details: object.DomainToDetailsPb( + &domain.ObjectDetails{ + Sequence: idp.Sequence, + EventDate: idp.ChangeDate, + ResourceOwner: idp.ResourceOwner, + }), + State: idpStateToPb(idp.State), + Name: idp.Name, + Type: idpTypeToPb(idp.Type), + Config: configToPb(idp), + } +} + +func idpStateToPb(state domain.IDPState) idp_pb.IDPState { + switch state { + case domain.IDPStateActive: + return idp_pb.IDPState_IDP_STATE_ACTIVE + case domain.IDPStateInactive: + return idp_pb.IDPState_IDP_STATE_INACTIVE + case domain.IDPStateUnspecified: + return idp_pb.IDPState_IDP_STATE_UNSPECIFIED + case domain.IDPStateMigrated: + return idp_pb.IDPState_IDP_STATE_MIGRATED + case domain.IDPStateRemoved: + return idp_pb.IDPState_IDP_STATE_REMOVED + default: + return idp_pb.IDPState_IDP_STATE_UNSPECIFIED + } +} + +func idpTypeToPb(idpType domain.IDPType) idp_pb.IDPType { + switch idpType { + case domain.IDPTypeOIDC: + return idp_pb.IDPType_IDP_TYPE_OIDC + case domain.IDPTypeJWT: + return idp_pb.IDPType_IDP_TYPE_JWT + case domain.IDPTypeOAuth: + return idp_pb.IDPType_IDP_TYPE_OAUTH + case domain.IDPTypeLDAP: + return idp_pb.IDPType_IDP_TYPE_LDAP + case domain.IDPTypeAzureAD: + return idp_pb.IDPType_IDP_TYPE_AZURE_AD + case domain.IDPTypeGitHub: + return idp_pb.IDPType_IDP_TYPE_GITHUB + case domain.IDPTypeGitHubEnterprise: + return idp_pb.IDPType_IDP_TYPE_GITHUB_ES + case domain.IDPTypeGitLab: + return idp_pb.IDPType_IDP_TYPE_GITLAB + case domain.IDPTypeGitLabSelfHosted: + return idp_pb.IDPType_IDP_TYPE_GITLAB_SELF_HOSTED + case domain.IDPTypeGoogle: + return idp_pb.IDPType_IDP_TYPE_GOOGLE + case domain.IDPTypeApple: + return idp_pb.IDPType_IDP_TYPE_APPLE + case domain.IDPTypeSAML: + return idp_pb.IDPType_IDP_TYPE_SAML + case domain.IDPTypeUnspecified: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED + default: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED + } +} + +func configToPb(config *query.IDPTemplate) *idp_pb.IDPConfig { + idpConfig := &idp_pb.IDPConfig{ + Options: &idp_pb.Options{ + IsLinkingAllowed: config.IsLinkingAllowed, + IsCreationAllowed: config.IsCreationAllowed, + IsAutoCreation: config.IsAutoCreation, + IsAutoUpdate: config.IsAutoUpdate, + AutoLinking: autoLinkingOptionToPb(config.AutoLinking), + }, + } + if config.OAuthIDPTemplate != nil { + oauthConfigToPb(idpConfig, config.OAuthIDPTemplate) + return idpConfig + } + if config.OIDCIDPTemplate != nil { + oidcConfigToPb(idpConfig, config.OIDCIDPTemplate) + return idpConfig + } + if config.JWTIDPTemplate != nil { + jwtConfigToPb(idpConfig, config.JWTIDPTemplate) + return idpConfig + } + if config.AzureADIDPTemplate != nil { + azureConfigToPb(idpConfig, config.AzureADIDPTemplate) + return idpConfig + } + if config.GitHubIDPTemplate != nil { + githubConfigToPb(idpConfig, config.GitHubIDPTemplate) + return idpConfig + } + if config.GitHubEnterpriseIDPTemplate != nil { + githubEnterpriseConfigToPb(idpConfig, config.GitHubEnterpriseIDPTemplate) + return idpConfig + } + if config.GitLabIDPTemplate != nil { + gitlabConfigToPb(idpConfig, config.GitLabIDPTemplate) + return idpConfig + } + if config.GitLabSelfHostedIDPTemplate != nil { + gitlabSelfHostedConfigToPb(idpConfig, config.GitLabSelfHostedIDPTemplate) + return idpConfig + } + if config.GoogleIDPTemplate != nil { + googleConfigToPb(idpConfig, config.GoogleIDPTemplate) + return idpConfig + } + if config.LDAPIDPTemplate != nil { + ldapConfigToPb(idpConfig, config.LDAPIDPTemplate) + return idpConfig + } + if config.AppleIDPTemplate != nil { + appleConfigToPb(idpConfig, config.AppleIDPTemplate) + return idpConfig + } + if config.SAMLIDPTemplate != nil { + samlConfigToPb(idpConfig, config.SAMLIDPTemplate) + return idpConfig + } + return idpConfig +} + +func autoLinkingOptionToPb(linking domain.AutoLinkingOption) idp_pb.AutoLinkingOption { + switch linking { + case domain.AutoLinkingOptionUnspecified: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED + case domain.AutoLinkingOptionUsername: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME + case domain.AutoLinkingOptionEmail: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL + default: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED + } +} + +func oauthConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.OAuthIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Oauth{ + Oauth: &idp_pb.OAuthConfig{ + ClientId: template.ClientID, + AuthorizationEndpoint: template.AuthorizationEndpoint, + TokenEndpoint: template.TokenEndpoint, + UserEndpoint: template.UserEndpoint, + Scopes: template.Scopes, + IdAttribute: template.IDAttribute, + }, + } +} + +func oidcConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.OIDCIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Oidc{ + Oidc: &idp_pb.GenericOIDCConfig{ + ClientId: template.ClientID, + Issuer: template.Issuer, + Scopes: template.Scopes, + IsIdTokenMapping: template.IsIDTokenMapping, + }, + } +} + +func jwtConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.JWTIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Jwt{ + Jwt: &idp_pb.JWTConfig{ + JwtEndpoint: template.Endpoint, + Issuer: template.Issuer, + KeysEndpoint: template.KeysEndpoint, + HeaderName: template.HeaderName, + }, + } +} + +func azureConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.AzureADIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_AzureAd{ + AzureAd: &idp_pb.AzureADConfig{ + ClientId: template.ClientID, + Tenant: azureTenantToPb(template.Tenant), + EmailVerified: template.IsEmailVerified, + Scopes: template.Scopes, + }, + } +} + +func azureTenantToPb(tenant string) *idp_pb.AzureADTenant { + var tenantType idp_pb.IsAzureADTenantType + switch azuread.TenantType(tenant) { + case azuread.CommonTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_COMMON} + case azuread.OrganizationsTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_ORGANISATIONS} + case azuread.ConsumersTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_CONSUMERS} + default: + tenantType = &idp_pb.AzureADTenant_TenantId{TenantId: tenant} + } + return &idp_pb.AzureADTenant{Type: tenantType} +} + +func githubConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitHubIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Github{ + Github: &idp_pb.GitHubConfig{ + ClientId: template.ClientID, + Scopes: template.Scopes, + }, + } +} + +func githubEnterpriseConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitHubEnterpriseIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_GithubEs{ + GithubEs: &idp_pb.GitHubEnterpriseServerConfig{ + ClientId: template.ClientID, + AuthorizationEndpoint: template.AuthorizationEndpoint, + TokenEndpoint: template.TokenEndpoint, + UserEndpoint: template.UserEndpoint, + Scopes: template.Scopes, + }, + } +} + +func gitlabConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitLabIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Gitlab{ + Gitlab: &idp_pb.GitLabConfig{ + ClientId: template.ClientID, + Scopes: template.Scopes, + }, + } +} + +func gitlabSelfHostedConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitLabSelfHostedIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_GitlabSelfHosted{ + GitlabSelfHosted: &idp_pb.GitLabSelfHostedConfig{ + ClientId: template.ClientID, + Issuer: template.Issuer, + Scopes: template.Scopes, + }, + } +} + +func googleConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GoogleIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Google{ + Google: &idp_pb.GoogleConfig{ + ClientId: template.ClientID, + Scopes: template.Scopes, + }, + } +} + +func ldapConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.LDAPIDPTemplate) { + var timeout *durationpb.Duration + if template.Timeout != 0 { + timeout = durationpb.New(template.Timeout) + } + idpConfig.Config = &idp_pb.IDPConfig_Ldap{ + Ldap: &idp_pb.LDAPConfig{ + Servers: template.Servers, + StartTls: template.StartTLS, + BaseDn: template.BaseDN, + BindDn: template.BindDN, + UserBase: template.UserBase, + UserObjectClasses: template.UserObjectClasses, + UserFilters: template.UserFilters, + Timeout: timeout, + Attributes: ldapAttributesToPb(template.LDAPAttributes), + }, + } +} + +func ldapAttributesToPb(attributes idp_rp.LDAPAttributes) *idp_pb.LDAPAttributes { + return &idp_pb.LDAPAttributes{ + IdAttribute: attributes.IDAttribute, + FirstNameAttribute: attributes.FirstNameAttribute, + LastNameAttribute: attributes.LastNameAttribute, + DisplayNameAttribute: attributes.DisplayNameAttribute, + NickNameAttribute: attributes.NickNameAttribute, + PreferredUsernameAttribute: attributes.PreferredUsernameAttribute, + EmailAttribute: attributes.EmailAttribute, + EmailVerifiedAttribute: attributes.EmailVerifiedAttribute, + PhoneAttribute: attributes.PhoneAttribute, + PhoneVerifiedAttribute: attributes.PhoneVerifiedAttribute, + PreferredLanguageAttribute: attributes.PreferredLanguageAttribute, + AvatarUrlAttribute: attributes.AvatarURLAttribute, + ProfileAttribute: attributes.ProfileAttribute, + } +} + +func appleConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.AppleIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Apple{ + Apple: &idp_pb.AppleConfig{ + ClientId: template.ClientID, + TeamId: template.TeamID, + KeyId: template.KeyID, + Scopes: template.Scopes, + }, + } +} + +func samlConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.SAMLIDPTemplate) { + nameIDFormat := idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT + if template.NameIDFormat.Valid { + nameIDFormat = nameIDToPb(template.NameIDFormat.V) + } + idpConfig.Config = &idp_pb.IDPConfig_Saml{ + Saml: &idp_pb.SAMLConfig{ + MetadataXml: template.Metadata, + Binding: bindingToPb(template.Binding), + WithSignedRequest: template.WithSignedRequest, + NameIdFormat: nameIDFormat, + TransientMappingAttributeName: gu.Ptr(template.TransientMappingAttributeName), + }, + } +} + +func bindingToPb(binding string) idp_pb.SAMLBinding { + switch binding { + case "": + return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED + case saml.HTTPPostBinding: + return idp_pb.SAMLBinding_SAML_BINDING_POST + case saml.HTTPRedirectBinding: + return idp_pb.SAMLBinding_SAML_BINDING_REDIRECT + case saml.HTTPArtifactBinding: + return idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT + default: + return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED + } +} + +func nameIDToPb(format domain.SAMLNameIDFormat) idp_pb.SAMLNameIDFormat { + switch format { + case domain.SAMLNameIDFormatUnspecified: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED + case domain.SAMLNameIDFormatEmailAddress: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_EMAIL_ADDRESS + case domain.SAMLNameIDFormatPersistent: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT + case domain.SAMLNameIDFormatTransient: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT + default: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED + } +} diff --git a/internal/api/grpc/idp/v2/query_integration_test.go b/internal/api/grpc/idp/v2/query_integration_test.go new file mode 100644 index 0000000000..1135e33547 --- /dev/null +++ b/internal/api/grpc/idp/v2/query_integration_test.go @@ -0,0 +1,235 @@ +//go:build integration + +package idp_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/idp/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" +) + +type idpAttr struct { + ID string + Name string + Details *object.Details +} + +func TestServer_GetIDPByID(t *testing.T) { + type args struct { + ctx context.Context + req *idp.GetIDPByIDRequest + dep func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr + } + tests := []struct { + name string + args args + want *idp.GetIDPByIDResponse + wantErr bool + }{ + { + name: "idp by ID, no id provided", + args: args{ + IamCTX, + &idp.GetIDPByIDRequest{ + Id: "", + }, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + return nil + }, + }, + wantErr: true, + }, + { + name: "idp by ID, not found", + args: args{ + IamCTX, + &idp.GetIDPByIDRequest{ + Id: "unknown", + }, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + return nil + }, + }, + wantErr: true, + }, + { + name: "idp by ID, instance, ok", + args: args{ + IamCTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddGenericOAuthIDP(ctx, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + 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_OAUTH, + Config: &idp.IDPConfig{ + Config: &idp.IDPConfig_Oauth{ + Oauth: &idp.OAuthConfig{ + ClientId: "clientID", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + }, + }, + Options: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }, + }, + }, + }, + { + name: "idp by ID, instance, no permission", + args: args{ + UserCTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddGenericOAuthIDP(IamCTX, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + wantErr: true, + }, + { + name: "idp by ID, org, ok", + args: args{ + CTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddOrgGenericOAuthIDP(ctx, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + 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_OAUTH, + Config: &idp.IDPConfig{ + Config: &idp.IDPConfig_Oauth{ + Oauth: &idp.OAuthConfig{ + ClientId: "clientID", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + }, + }, + Options: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }, + }, + }, + }, + { + name: "idp by ID, org, no permission", + args: args{ + UserCTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddOrgGenericOAuthIDP(CTX, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + idpAttr := tt.args.dep(tt.args.ctx, tt.args.req) + retryDuration := time.Minute + if ctxDeadline, ok := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, getErr := Client.GetIDPByID(tt.args.ctx, tt.args.req) + assertErr := assert.NoError + if tt.wantErr { + assertErr = assert.Error + } + assertErr(ttt, getErr) + if getErr != nil { + return + } + + // set provided info from creation + tt.want.Idp.Details = idpAttr.Details + tt.want.Idp.Name = idpAttr.Name + tt.want.Idp.Id = idpAttr.ID + + // first check for details, mgmt and admin api don't fill the details correctly + integration.AssertDetails(t, tt.want.Idp, got.Idp) + // then set details + tt.want.Idp.Details = got.Idp.Details + // to check the rest of the content + assert.Equal(ttt, tt.want.Idp, got.Idp) + }, retryDuration, time.Second) + }) + } +} diff --git a/internal/api/grpc/idp/v2/server.go b/internal/api/grpc/idp/v2/server.go new file mode 100644 index 0000000000..246e980434 --- /dev/null +++ b/internal/api/grpc/idp/v2/server.go @@ -0,0 +1,56 @@ +package idp + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/idp/v2" +) + +var _ idp.IdentityProviderServiceServer = (*Server)(nil) + +type Server struct { + idp.UnimplementedIdentityProviderServiceServer + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + idp.RegisterIdentityProviderServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return idp.IdentityProviderService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return idp.IdentityProviderService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return idp.IdentityProviderService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return idp.RegisterIdentityProviderServiceHandler +} diff --git a/internal/api/grpc/idp/v2/server_integration_test.go b/internal/api/grpc/idp/v2/server_integration_test.go new file mode 100644 index 0000000000..9e8f44a311 --- /dev/null +++ b/internal/api/grpc/idp/v2/server_integration_test.go @@ -0,0 +1,40 @@ +//go:build integration + +package idp_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/zitadel/zitadel/internal/integration" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" +) + +var ( + CTX context.Context + IamCTX context.Context + UserCTX context.Context + SystemCTX context.Context + ErrCTX context.Context + Tester *integration.Tester + Client idp_pb.IdentityProviderServiceClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, errCtx, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + UserCTX = Tester.WithAuthorization(ctx, integration.Login) + IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + CTX, ErrCTX = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx + Client = Tester.Client.IDPv2 + return m.Run() + }()) +} diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index f013015258..66b659d1ea 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -149,7 +149,7 @@ func (s *Server) GetProviderByID(ctx context.Context, req *mgmt_pb.GetProviderBy if err != nil { return nil, err } - idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, orgIDQuery) + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, nil, orgIDQuery) if err != nil { return nil, err } diff --git a/internal/api/ui/login/policy_handler.go b/internal/api/ui/login/policy_handler.go index e5e6336068..2ff4ac9b78 100644 --- a/internal/api/ui/login/policy_handler.go +++ b/internal/api/ui/login/policy_handler.go @@ -18,7 +18,7 @@ func (l *Login) getOrgDomainPolicy(r *http.Request, orgID string) (*query.Domain } func (l *Login) getIDPByID(r *http.Request, id string) (*query.IDPTemplate, error) { - return l.query.IDPTemplateByID(r.Context(), false, id, false) + return l.query.IDPTemplateByID(r.Context(), false, id, false, nil) } func (l *Login) getLoginPolicy(r *http.Request, orgID string) (*query.LoginPolicy, error) { diff --git a/internal/domain/permission.go b/internal/domain/permission.go index cf2d02d426..75d7f792ff 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -33,4 +33,6 @@ const ( PermissionUserCredentialWrite = "user.credential.write" PermissionSessionWrite = "session.write" PermissionSessionDelete = "session.delete" + PermissionIDPRead = "iam.idp.read" + PermissionOrgIDPRead = "org.idp.read" ) diff --git a/internal/integration/client.go b/internal/integration/client.go index 947c11508b..a69ce6a1ee 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -24,22 +24,24 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/idp/providers/saml" - "github.com/zitadel/zitadel/internal/repository/idp" + idp_rp "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/idp" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" @@ -69,6 +71,7 @@ type Client struct { FeatureV2 feature.FeatureServiceClient UserSchemaV3 schema.UserSchemaServiceClient WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient + IDPv2 idp_pb.IdentityProviderServiceClient } func newClient(cc *grpc.ClientConn) Client { @@ -93,6 +96,7 @@ func newClient(cc *grpc.ClientConn) Client { FeatureV2: feature.NewFeatureServiceClient(cc), UserSchemaV3: schema.NewUserSchemaServiceClient(cc), WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc), + IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), } } @@ -367,6 +371,28 @@ func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, c return resp.GetDetails() } +func (s *Tester) AddGenericOAuthIDP(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse { + resp, err := s.Client.Admin.AddGenericOAuthProvider(ctx, &admin.AddGenericOAuthProviderRequest{ + Name: name, + ClientId: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }) + logging.OnError(err).Fatal("create generic OAuth idp") + return resp +} + func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) string { ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Commands.AddInstanceGenericOAuthProvider(ctx, command.GenericOAuthProvider{ @@ -378,7 +404,7 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) stri UserEndpoint: "https://api.example.com/user", Scopes: []string{"openid", "profile", "email"}, IDAttribute: "id", - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -389,6 +415,28 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) stri return id } +func (s *Tester) AddOrgGenericOAuthIDP(ctx context.Context, name string) *mgmt.AddGenericOAuthProviderResponse { + resp, err := s.Client.Mgmt.AddGenericOAuthProvider(ctx, &mgmt.AddGenericOAuthProviderRequest{ + Name: name, + ClientId: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }) + logging.OnError(err).Fatal("create generic OAuth idp") + return resp +} + func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, orgID string) string { ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, orgID, @@ -401,7 +449,7 @@ func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, o UserEndpoint: "https://api.example.com/user", Scopes: []string{"openid", "profile", "email"}, IDAttribute: "id", - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -417,7 +465,7 @@ func (s *Tester) AddSAMLProvider(t *testing.T, ctx context.Context) string { id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ Name: "saml-idp", Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -435,7 +483,7 @@ func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context, tran Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), TransientMappingAttributeName: transientMappingAttributeName, - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -452,7 +500,7 @@ func (s *Tester) AddSAMLPostProvider(t *testing.T, ctx context.Context) string { Name: "saml-idp-post", Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 2b835f0e69..e3250f1ae7 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -712,8 +712,29 @@ var ( } ) -// IDPTemplateByID searches for the requested id -func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) { +// 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...) + if err != nil { + return nil, err + } + if permissionCheck != nil { + switch idp.OwnerType { + case domain.IdentityProviderTypeSystem: + if err := permissionCheck(ctx, domain.PermissionIDPRead, idp.ResourceOwner, idp.ID); err != nil { + return nil, err + } + case domain.IdentityProviderTypeOrg: + if err := permissionCheck(ctx, domain.PermissionOrgIDPRead, idp.ResourceOwner, idp.ID); err != nil { + return nil, err + } + } + } + return idp, nil +} + +// idpTemplateByID searches for the requested id +func (q *Queries) idpTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/pkg/grpc/idp/v2/idp.go b/pkg/grpc/idp/v2/idp.go new file mode 100644 index 0000000000..514f6c5e33 --- /dev/null +++ b/pkg/grpc/idp/v2/idp.go @@ -0,0 +1,4 @@ +package idp + +type IsIDPConfig = isIDPConfig_Config +type IsAzureADTenantType = isAzureADTenant_Type diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto new file mode 100644 index 0000000000..784e717d3a --- /dev/null +++ b/proto/zitadel/idp/v2/idp.proto @@ -0,0 +1,391 @@ +syntax = "proto3"; + +package zitadel.idp.v2; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/object/v2/object.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/duration.proto"; + + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/idp/v2;idp"; + +message IDP { + // Unique identifier for the identity provider. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + zitadel.object.v2.Details details = 2; + // Current state of the identity provider. + IDPState state = 3; + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Google\""; + } + ]; + // Type of the identity provider, for example OIDC, JWT, LDAP and SAML. + IDPType type = 5; + // Configuration for the type of the identity provider. + IDPConfig config = 6; +} + +enum IDPState { + IDP_STATE_UNSPECIFIED = 0; + IDP_STATE_ACTIVE = 1; + IDP_STATE_INACTIVE = 2; + IDP_STATE_REMOVED = 3; + IDP_STATE_MIGRATED = 4; +} + +enum IDPType { + IDP_TYPE_UNSPECIFIED = 0; + IDP_TYPE_OIDC = 1; + IDP_TYPE_JWT = 2; + IDP_TYPE_LDAP = 3; + IDP_TYPE_OAUTH = 4; + IDP_TYPE_AZURE_AD = 5; + IDP_TYPE_GITHUB = 6; + IDP_TYPE_GITHUB_ES = 7; + IDP_TYPE_GITLAB = 8; + IDP_TYPE_GITLAB_SELF_HOSTED = 9; + IDP_TYPE_GOOGLE = 10; + IDP_TYPE_APPLE = 11; + IDP_TYPE_SAML = 12; +} + +enum SAMLBinding { + SAML_BINDING_UNSPECIFIED = 0; + SAML_BINDING_POST = 1; + SAML_BINDING_REDIRECT = 2; + SAML_BINDING_ARTIFACT = 3; +} + +enum SAMLNameIDFormat { + SAML_NAME_ID_FORMAT_UNSPECIFIED = 0; + SAML_NAME_ID_FORMAT_EMAIL_ADDRESS = 1; + SAML_NAME_ID_FORMAT_PERSISTENT = 2; + SAML_NAME_ID_FORMAT_TRANSIENT = 3; +} + +message IDPConfig { + Options options = 1; + oneof config { + LDAPConfig ldap = 2; + GoogleConfig google = 3; + OAuthConfig oauth = 4; + GenericOIDCConfig oidc = 5; + JWTConfig jwt = 6; + GitHubConfig github = 7; + GitHubEnterpriseServerConfig github_es = 8; + GitLabConfig gitlab = 9; + GitLabSelfHostedConfig gitlab_self_hosted = 10; + AzureADConfig azure_ad = 11; + AppleConfig apple = 12; + SAMLConfig saml = 13; + } +} + +message JWTConfig { + // The endpoint where the JWT can be extracted. + string jwt_endpoint = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + } + ]; + // The issuer of the JWT (for validation). + string issuer = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + } + ]; + // The endpoint to the key (JWK) which is used to sign the JWT with. + string keys_endpoint = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/keys\""; + } + ]; + // The name of the header where the JWT is sent in, default is authorization. + string header_name = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"x-auth-token\""; + } + ]; +} + +message OAuthConfig { + // Client id generated by the identity provider. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The endpoint where ZITADEL send the user to authenticate. + string authorization_endpoint = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/o/oauth2/v2/auth\""; + } + ]; + // The endpoint where ZITADEL can get the token. + string token_endpoint = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://oauth2.googleapis.com/token\""; + } + ]; + // The endpoint where ZITADEL can get the user information. + string user_endpoint = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://openidconnect.googleapis.com/v1/userinfo\""; + } + ]; + // The scopes requested by ZITADEL during the request on the identity provider. + repeated string scopes = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; + // Defines how the attribute is called where ZITADEL can get the id of the user. + string id_attribute = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"user_id\""; + } + ]; +} + +message GenericOIDCConfig { + // The OIDC issuer of the identity provider. + string issuer = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/\""; + } + ]; + // Client id generated by the identity provider. + string client_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request on the identity provider. + repeated string scopes = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; + // If true, provider information get mapped from the id token, not from the userinfo endpoint. + bool is_id_token_mapping = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + } + ]; +} + +message GitHubConfig { + // The client ID of the GitHub App. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to GitHub. + repeated string scopes = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GitHubEnterpriseServerConfig { + // The client ID of the GitHub App. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + string authorization_endpoint = 2; + string token_endpoint = 3; + string user_endpoint = 4; + // The scopes requested by ZITADEL during the request to GitHub. + repeated string scopes = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GoogleConfig { + // Client id of the Google application. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to Google. + repeated string scopes = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GitLabConfig { + // Client id of the GitLab application. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to GitLab. + repeated string scopes = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GitLabSelfHostedConfig { + string issuer = 1; + // Client id of the GitLab application. + string client_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to GitLab. + repeated string scopes = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message LDAPConfig { + repeated string servers = 1; + bool start_tls = 2; + string base_dn = 3; + string bind_dn = 4; + string user_base = 5; + repeated string user_object_classes = 6; + repeated string user_filters = 7; + google.protobuf.Duration timeout = 8; + LDAPAttributes attributes = 9; +} + +message SAMLConfig { + // Metadata of the SAML identity provider. + bytes metadata_xml = 1; + // Binding which defines the type of communication with the identity provider. + SAMLBinding binding = 2; + // Boolean which defines if the authentication requests are signed. + bool with_signed_request = 3; + // `nameid-format` for the SAML Request. + SAMLNameIDFormat name_id_format = 4; + // Optional name of the attribute, which will be used to map the user + // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. + optional string transient_mapping_attribute_name = 5; +} + +message AzureADConfig { + // Client id of the Azure AD application + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // Defines what user accounts should be able to login (Personal, Organizational, All). + AzureADTenant tenant = 2; + // Azure AD doesn't send if the email has been verified. Enable this if the user email should always be added verified in ZITADEL (no verification emails will be sent). + bool email_verified = 3; + // The scopes requested by ZITADEL during the request to Azure AD. + repeated string scopes = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\", \"User.Read\"]"; + } + ]; +} + +message Options { + // Enable if users should be able to link an existing ZITADEL user with an external account. + bool is_linking_allowed = 1; + // Enable if users should be able to create a new account in ZITADEL when using an external account. + bool is_creation_allowed = 2; + // Enable if a new account in ZITADEL should be created automatically when login with an external account. + bool is_auto_creation = 3; + // Enable if a the ZITADEL account fields should be updated automatically on each login. + bool is_auto_update = 4; + // Enable if users should get prompted to link an existing ZITADEL user to an external account if the selected attribute matches. + AutoLinkingOption auto_linking = 5 ; +} + +enum AutoLinkingOption { + // AUTO_LINKING_OPTION_UNSPECIFIED disables the auto linking prompt. + AUTO_LINKING_OPTION_UNSPECIFIED = 0; + // AUTO_LINKING_OPTION_USERNAME will use the username of the external user to check for a corresponding ZITADEL user. + AUTO_LINKING_OPTION_USERNAME = 1; + // AUTO_LINKING_OPTION_EMAIL will use the email of the external user to check for a corresponding ZITADEL user with the same verified email + // Note that in case multiple users match, no prompt will be shown. + AUTO_LINKING_OPTION_EMAIL = 2; +} + +message LDAPAttributes { + string id_attribute = 1 [(validate.rules).string = {max_len: 200}]; + string first_name_attribute = 2 [(validate.rules).string = {max_len: 200}]; + string last_name_attribute = 3 [(validate.rules).string = {max_len: 200}]; + string display_name_attribute = 4 [(validate.rules).string = {max_len: 200}]; + string nick_name_attribute = 5 [(validate.rules).string = {max_len: 200}]; + string preferred_username_attribute = 6 [(validate.rules).string = {max_len: 200}]; + string email_attribute = 7 [(validate.rules).string = {max_len: 200}]; + string email_verified_attribute = 8 [(validate.rules).string = {max_len: 200}]; + string phone_attribute = 9 [(validate.rules).string = {max_len: 200}]; + string phone_verified_attribute = 10 [(validate.rules).string = {max_len: 200}]; + string preferred_language_attribute = 11 [(validate.rules).string = {max_len: 200}]; + string avatar_url_attribute = 12 [(validate.rules).string = {max_len: 200}]; + string profile_attribute = 13 [(validate.rules).string = {max_len: 200}]; +} + +enum AzureADTenantType { + AZURE_AD_TENANT_TYPE_COMMON = 0; + AZURE_AD_TENANT_TYPE_ORGANISATIONS = 1; + AZURE_AD_TENANT_TYPE_CONSUMERS = 2; +} + +message AzureADTenant { + oneof type { + AzureADTenantType tenant_type = 1; + string tenant_id = 2; + } +} + +message AppleConfig { + // Client id (App ID or Service ID) provided by Apple. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"com.client.id\""; + } + ]; + // Team ID provided by Apple. + string team_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ALT03JV3OS\""; + } + ]; + // ID of the private key generated by Apple. + string key_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"OGKDK25KD\""; + } + ]; + // The scopes requested by ZITADEL during the request to Apple. + repeated string scopes = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"name\", \"email\"]"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/idp/v2/idp_service.proto b/proto/zitadel/idp/v2/idp_service.proto new file mode 100644 index 0000000000..2d5306cea6 --- /dev/null +++ b/proto/zitadel/idp/v2/idp_service.proto @@ -0,0 +1,136 @@ +syntax = "proto3"; + +package zitadel.idp.v2; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/object/v2/object.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/idp/v2/idp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/idp/v2;idp"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Identity Provider Service"; + version: "2.0"; + description: "This API is intended to manage identity providers (IdPs) in a ZITADEL instance."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +service IdentityProviderService { + + // Get identity provider (IdP) by ID + // + // Returns an identity provider (social/enterprise login) by its ID, which can be of the type Google, AzureAD, etc. + rpc GetIDPByID (GetIDPByIDRequest) returns (GetIDPByIDResponse) { + option (google.api.http) = { + get: "/v2/idps/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } +} + +message GetIDPByIDRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetIDPByIDResponse { + zitadel.idp.v2.IDP idp = 1; +} diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto index 08d57e93e5..228899310a 100644 --- a/proto/zitadel/resources/action/v3alpha/action_service.proto +++ b/proto/zitadel/resources/action/v3alpha/action_service.proto @@ -47,7 +47,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { consumes: "application/grpc-web+proto"; produces: "application/grpc-web+proto"; - host: "${ZITADEL_DOMAIN}"; + host: "$CUSTOM-DOMAIN"; base_path: "/resources/v3alpha/actions"; external_docs: { @@ -60,8 +60,8 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { value: { type: TYPE_OAUTH2; flow: FLOW_ACCESS_CODE; - authorization_url: "${ZITADEL_DOMAIN}/oauth/v2/authorize"; - token_url: "${ZITADEL_DOMAIN}/oauth/v2/token"; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; scopes: { scope: { key: "openid"; @@ -135,7 +135,7 @@ service ZITADELActions { description: "Target successfully created"; schema: { json_schema: { - ref: "#/definitions/CreateTargetResponse"; + ref: "#/definitions/v3alphaCreateTargetResponse"; } } }; @@ -278,7 +278,7 @@ service ZITADELActions { description: "Execution successfully updated or left unchanged"; schema: { json_schema: { - ref: "#/definitions/SetExecutionResponse"; + ref: "#/definitions/v3alphaSetExecutionResponse"; } } }; diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto index c79424095b..72263c7b73 100644 --- a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto +++ b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto @@ -42,7 +42,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { consumes: "application/grpc-web+proto"; produces: "application/grpc-web+proto"; - host: "${ZITADEL_DOMAIN}"; + host: "$CUSTOM-DOMAIN"; base_path: "/resources/v3alpha/web_keys"; external_docs: {