From 2dc016ea3b533e95fbd07323451dc237cafc16d1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 11 May 2023 10:18:14 +0200 Subject: [PATCH 1/3] feat(console): device code (#5771) * feat: device code * device code, create stepper * rm logs * app setup with device code * remove redirects if grant type is device code only * add device code app e2e --------- Co-authored-by: Fabi Co-authored-by: Elio Bischof --- .../app-auth-method-radio.component.html | 14 ++-- .../app-auth-method-radio.component.scss | 6 +- .../app-auth-method-radio.component.ts | 2 +- .../apps/app-create/app-create.component.html | 22 +++--- .../apps/app-create/app-create.component.ts | 14 +++- .../apps/app-detail/app-detail.component.ts | 29 +++++-- .../app/pages/projects/apps/authmethods.ts | 79 +++++++++++++++++-- console/src/assets/i18n/de.json | 7 +- console/src/assets/i18n/en.json | 7 +- console/src/assets/i18n/es.json | 7 +- console/src/assets/i18n/fr.json | 7 +- console/src/assets/i18n/it.json | 7 +- console/src/assets/i18n/ja.json | 7 +- console/src/assets/i18n/pl.json | 7 +- console/src/assets/i18n/zh.json | 7 +- .../e2e/applications/applications.cy.ts | 36 ++++++++- 16 files changed, 212 insertions(+), 46 deletions(-) diff --git a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.html b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.html index 5ab289448d..08b278854d 100644 --- a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.html +++ b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.html @@ -30,19 +30,23 @@
- {{ 'APP.OIDC.RESPONSETYPE' | translate }} + {{ 'APP.OIDC.RESPONSETYPE' | translate }} {{ 'APP.OIDC.RESPONSE.' + method.responseType.toString() | translate }}
- {{ 'APP.GRANT' | translate }} - {{ 'APP.OIDC.GRANT.' + method.grantType.toString() | translate }} + {{ 'APP.GRANT' | translate }} + {{ + 'APP.OIDC.GRANT.' + grant.toString() | translate + }}
- {{ 'APP.AUTHMETHOD' | translate }} + {{ 'APP.AUTHMETHOD' | translate }} {{ 'APP.OIDC.AUTHMETHOD.' + method.authMethod.toString() | translate }}
- {{ 'APP.AUTHMETHOD' | translate }} + {{ 'APP.AUTHMETHOD' | translate }} {{ 'APP.API.AUTHMETHOD.' + method.apiAuthMethod.toString() | translate }}
diff --git a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss index f7ddc2d147..0abe726237 100644 --- a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss +++ b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss @@ -155,7 +155,11 @@ white-space: nowrap; } - :first-child { + .space { + margin-left: 0.5rem; + } + + .row-entry { margin-right: 1rem; overflow: hidden; text-overflow: ellipsis; diff --git a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.ts b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.ts index 48d232850c..4e9f64ab10 100644 --- a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.ts +++ b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.ts @@ -14,7 +14,7 @@ export interface RadioItemAuthType { prefix: string; background: string; responseType?: OIDCResponseType; - grantType?: OIDCGrantType; + grantType?: OIDCGrantType[]; authMethod?: OIDCAuthMethodType; apiAuthMethod?: APIAuthMethodType; recommended?: boolean; diff --git a/console/src/app/pages/projects/apps/app-create/app-create.component.html b/console/src/app/pages/projects/apps/app-create/app-create.component.html index 5e50ecb383..ab75a2c8f6 100644 --- a/console/src/app/pages/projects/apps/app-create/app-create.component.html +++ b/console/src/app/pages/projects/apps/app-create/app-create.component.html @@ -58,13 +58,9 @@ - + @@ -93,9 +89,11 @@ - - + {{ 'APP.OIDC.REDIRECTSECTION' | translate }}

{{ 'APP.OIDC.REDIRECTTITLE' | translate }}

@@ -431,7 +429,13 @@ -
+
{ beforeEach(() => { @@ -17,15 +18,15 @@ describe('applications', () => { beforeEach(`ensure it doesn't exist already`, () => { cy.get('@ctx').then((ctx) => { cy.get('@projectId').then((projectId) => { - ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testAppName); + ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testPKCEAppName); cy.visit(`/projects/${projectId}`); }); }); }); - it('add app', () => { + it('add web pkce app', () => { cy.get('[data-e2e="app-card-add"]').should('be.visible').click(); - cy.get('[formcontrolname="name"]').focus().type(testAppName); + cy.get('[formcontrolname="name"]').focus().type(testPKCEAppName); cy.get('[for="WEB"]').click(); cy.get('[data-e2e="continue-button-nameandtype"]').click(); cy.get('[for="PKCE"]').should('be.visible').click(); @@ -43,6 +44,33 @@ describe('applications', () => { }); }); + describe('add native device code app', () => { + beforeEach(`ensure it doesn't exist already`, () => { + cy.get('@ctx').then((ctx) => { + cy.get('@projectId').then((projectId) => { + ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testDEVICECODEAppName); + cy.visit(`/projects/${projectId}`); + }); + }); + }); + + it('add device code app', () => { + cy.get('[data-e2e="app-card-add"]').should('be.visible').click(); + cy.get('[formcontrolname="name"]').focus().type(testDEVICECODEAppName); + cy.get('[for="N"]').click(); + cy.get('[data-e2e="continue-button-nameandtype"]').click(); + cy.get('[for="DEVICECODE"]').should('be.visible').click(); + cy.get('[data-e2e="continue-button-authmethod"]').click(); + cy.get('[data-e2e="create-button"]').click(); + cy.get('[id*=overlay]').should('exist'); + cy.shouldConfirmSuccess(); + const expectClientId = new RegExp(`^.*[0-9]+\\@${testProjectName}.*$`); + cy.get('[data-e2e="client-id-copy"]').click(); + cy.contains('[data-e2e="client-id"]', expectClientId); + cy.clipboardMatches(expectClientId); + }); + }); + describe('edit app', () => { it('should configure an application to enable dev mode'); it('should configure an application to put user roles and info inside id token'); From c07411e3142a0a88866d31536208336cee0d5864 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 11 May 2023 10:58:35 +0200 Subject: [PATCH 2/3] fix: only reuse port for integration tests (#5817) * fix: only reuse port for integration tests * exclude default listenConfig from integration build --- cmd/start/start.go | 13 +------------ cmd/start/start_port.go | 11 +++++++++++ cmd/start/start_port_integration.go | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 cmd/start/start_port.go create mode 100644 cmd/start/start_port_integration.go diff --git a/cmd/start/start.go b/cmd/start/start.go index 4ae1e15b08..a191998e1e 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -6,7 +6,6 @@ import ( _ "embed" "fmt" "math" - "net" "net/http" "os" "os/signal" @@ -22,7 +21,6 @@ import ( "github.com/zitadel/saml/pkg/provider" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" - "golang.org/x/sys/unix" "github.com/zitadel/zitadel/cmd/key" cmd_tls "github.com/zitadel/zitadel/cmd/tls" @@ -392,20 +390,11 @@ func startAPIs( return nil } -func reusePort(network, address string, conn syscall.RawConn) error { - return conn.Control(func(descriptor uintptr) { - err := syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) - if err != nil { - panic(err) - } - }) -} - func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls.Config, shutdown <-chan os.Signal) error { http2Server := &http2.Server{} http1Server := &http.Server{Handler: h2c.NewHandler(router, http2Server), TLSConfig: tlsConfig} - lc := &net.ListenConfig{Control: reusePort} + lc := listenConfig() lis, err := lc.Listen(ctx, "tcp", fmt.Sprintf(":%d", port)) if err != nil { return fmt.Errorf("tcp listener on %d failed: %w", port, err) diff --git a/cmd/start/start_port.go b/cmd/start/start_port.go new file mode 100644 index 0000000000..bb60fea250 --- /dev/null +++ b/cmd/start/start_port.go @@ -0,0 +1,11 @@ +//go:build !integration + +package start + +import ( + "net" +) + +func listenConfig() *net.ListenConfig { + return &net.ListenConfig{} +} diff --git a/cmd/start/start_port_integration.go b/cmd/start/start_port_integration.go new file mode 100644 index 0000000000..1c5763d6e9 --- /dev/null +++ b/cmd/start/start_port_integration.go @@ -0,0 +1,25 @@ +//go:build integration + +package start + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +func listenConfig() *net.ListenConfig { + return &net.ListenConfig{ + Control: reusePort, + } +} + +func reusePort(network, address string, conn syscall.RawConn) error { + return conn.Control(func(descriptor uintptr) { + err := syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) + if err != nil { + panic(err) + } + }) +} From 8d13f170e84ea7b25c001bf014f92617b6d549f8 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Thu, 11 May 2023 11:23:40 +0200 Subject: [PATCH 3/3] feat(api): new settings service (#5775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add v2alpha policies service * feat: add v2alpha policies service * fix: rename of attributes and messages in v2alpha api * fix: rename of attributes and messages in v2alpha api * fix: linter corrections * fix: review corrections * fix: review corrections * fix: review corrections * fix: review corrections * fix grpc * refactor: rename to settings and more * Apply suggestions from code review Co-authored-by: Fabi * add service to docs and rename legal settings * unit tests for converters * go mod tidy * ensure idp name and return list details * fix: use correct resource owner for active idps * change query to join --------- Co-authored-by: Livio Spring Co-authored-by: Fabi Co-authored-by: Tim Möhlmann --- build/zitadel/generate-grpc.sh | 12 + cmd/start/start.go | 4 + docs/docusaurus.config.js | 7 + docs/sidebars.js | 14 + internal/api/grpc/object/v2/converter.go | 13 + internal/api/grpc/settings/v2/server.go | 57 +++ internal/api/grpc/settings/v2/settings.go | 129 +++++ .../grpc/settings/v2/settings_converter.go | 189 +++++++ .../settings/v2/settings_converter_test.go | 461 ++++++++++++++++++ internal/domain/idp.go | 35 ++ internal/domain/policy_login.go | 27 +- internal/query/idp_login_policy_link.go | 48 +- internal/query/idp_login_policy_link_test.go | 24 +- proto/zitadel/object/v2alpha/object.proto | 7 + .../settings/v2alpha/branding_settings.proto | 81 +++ .../settings/v2alpha/domain_settings.proto | 33 ++ .../settings/v2alpha/legal_settings.proto | 40 ++ .../settings/v2alpha/lockout_settings.proto | 23 + .../settings/v2alpha/login_settings.proto | 143 ++++++ .../settings/v2alpha/password_settings.proto | 43 ++ proto/zitadel/settings/v2alpha/settings.proto | 13 + .../settings/v2alpha/settings_service.proto | 356 ++++++++++++++ 22 files changed, 1720 insertions(+), 39 deletions(-) create mode 100644 internal/api/grpc/settings/v2/server.go create mode 100644 internal/api/grpc/settings/v2/settings.go create mode 100644 internal/api/grpc/settings/v2/settings_converter.go create mode 100644 internal/api/grpc/settings/v2/settings_converter_test.go create mode 100644 proto/zitadel/settings/v2alpha/branding_settings.proto create mode 100644 proto/zitadel/settings/v2alpha/domain_settings.proto create mode 100644 proto/zitadel/settings/v2alpha/legal_settings.proto create mode 100644 proto/zitadel/settings/v2alpha/lockout_settings.proto create mode 100644 proto/zitadel/settings/v2alpha/login_settings.proto create mode 100644 proto/zitadel/settings/v2alpha/password_settings.proto create mode 100644 proto/zitadel/settings/v2alpha/settings.proto create mode 100644 proto/zitadel/settings/v2alpha/settings_service.proto diff --git a/build/zitadel/generate-grpc.sh b/build/zitadel/generate-grpc.sh index 891125cb6a..865952fb66 100755 --- a/build/zitadel/generate-grpc.sh +++ b/build/zitadel/generate-grpc.sh @@ -96,4 +96,16 @@ protoc \ --validate_out=lang=go:${GOPATH}/src \ ${PROTO_PATH}/session/v2alpha/session_service.proto +protoc \ + -I=/proto/include \ + --grpc-gateway_out ${GOPATH}/src \ + --grpc-gateway_opt logtostderr=true \ + --grpc-gateway_opt allow_delete_body=true \ + --openapiv2_out ${OPENAPI_PATH} \ + --openapiv2_opt logtostderr=true \ + --openapiv2_opt allow_delete_body=true \ + --zitadel_out=${GOPATH}/src \ + --validate_out=lang=go:${GOPATH}/src \ + ${PROTO_PATH}/settings/v2alpha/settings_service.proto + echo "done generating grpc" diff --git a/cmd/start/start.go b/cmd/start/start.go index a191998e1e..0d2d973690 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -33,6 +33,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/auth" "github.com/zitadel/zitadel/internal/api/grpc/management" "github.com/zitadel/zitadel/internal/api/grpc/session/v2" + "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" "github.com/zitadel/zitadel/internal/api/grpc/system" "github.com/zitadel/zitadel/internal/api/grpc/user/v2" http_util "github.com/zitadel/zitadel/internal/api/http" @@ -337,6 +338,9 @@ func startAPIs( if err := apis.RegisterService(ctx, session.CreateServer(commands, queries, permissionCheck)); err != nil { return err } + if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil { + return err + } instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 5ed0a099d8..409f3905e3 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -273,6 +273,13 @@ module.exports = { sidebarOptions: { groupPathsBy: "tag", }, + }, + settings: { + specPath: ".artifacts/openapi/zitadel/settings/v2alpha/settings_service.swagger.json", + outputDir: "docs/apis/settings_service", + sidebarOptions: { + groupPathsBy: "tag", + }, } } }, diff --git a/docs/sidebars.js b/docs/sidebars.js index 25e84b4b91..54ca65bb18 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -426,6 +426,20 @@ module.exports = { }, items: require("./docs/apis/session_service/sidebar.js"), }, + { + type: "category", + label: "Settings Lifecycle (Alpha)", + link: { + type: "generated-index", + title: "Settings Service API (Alpha)", + slug: "/apis/settings_service", + description: + "This API is intended to manage settings in a ZITADEL instance.\n"+ + "\n"+ + "This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.", + }, + items: require("./docs/apis/settings_service/sidebar.js"), + }, { type: "category", label: "Assets", diff --git a/internal/api/grpc/object/v2/converter.go b/internal/api/grpc/object/v2/converter.go index feb8dbd62a..6c03e79a8c 100644 --- a/internal/api/grpc/object/v2/converter.go +++ b/internal/api/grpc/object/v2/converter.go @@ -1,8 +1,11 @@ package object import ( + "context" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" @@ -36,3 +39,13 @@ func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) } return query.Offset, uint64(query.Limit), query.Asc } + +func ResourceOwnerFromReq(ctx context.Context, req *object.RequestContext) string { + if req.GetInstance() { + return authz.GetInstance(ctx).InstanceID() + } + if req.GetOrgId() != "" { + return req.GetOrgId() + } + return authz.GetCtxData(ctx).OrgID +} diff --git a/internal/api/grpc/settings/v2/server.go b/internal/api/grpc/settings/v2/server.go new file mode 100644 index 0000000000..ea6ebd4a25 --- /dev/null +++ b/internal/api/grpc/settings/v2/server.go @@ -0,0 +1,57 @@ +package settings + +import ( + "context" + + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/assets" + "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/query" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha" +) + +var _ settings.SettingsServiceServer = (*Server)(nil) + +type Server struct { + settings.UnimplementedSettingsServiceServer + command *command.Commands + query *query.Queries + assetsAPIDomain func(context.Context) string +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + externalSecure bool, +) *Server { + return &Server{ + command: command, + query: query, + assetsAPIDomain: assets.AssetAPI(externalSecure), + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + settings.RegisterSettingsServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return settings.SettingsService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return settings.SettingsService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return settings.SettingsService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return settings.RegisterSettingsServiceHandler +} diff --git a/internal/api/grpc/settings/v2/settings.go b/internal/api/grpc/settings/v2/settings.go new file mode 100644 index 0000000000..bf00d8084c --- /dev/null +++ b/internal/api/grpc/settings/v2/settings.go @@ -0,0 +1,129 @@ +package settings + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/api/grpc/text" + "github.com/zitadel/zitadel/internal/query" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha" +) + +func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) { + current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetLoginSettingsResponse{ + Settings: loginSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.OrgID, + }, + }, nil +} + +func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) { + current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetPasswordComplexitySettingsResponse{ + Settings: passwordSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) { + current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetBrandingSettingsResponse{ + Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) { + current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetDomainSettingsResponse{ + Settings: domainSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) { + current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetLegalAndSupportSettingsResponse{ + Settings: legalAndSupportSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) { + current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetLockoutSettingsResponse{ + Settings: lockoutSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) { + links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{}, false) + if err != nil { + return nil, err + } + + return &settings.GetActiveIdentityProvidersResponse{ + Details: object.ToListDetails(links.SearchResponse), + IdentityProviders: identityProvidersToPb(links.Links), + }, nil +} + +func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { + langs, err := s.query.Languages(ctx) + if err != nil { + return nil, err + } + instance := authz.GetInstance(ctx) + return &settings.GetGeneralSettingsResponse{ + SupportedLanguages: text.LanguageTagsToStrings(langs), + DefaultOrgId: instance.DefaultOrganisationID(), + DefaultLanguage: instance.DefaultLanguage().String(), + }, nil +} diff --git a/internal/api/grpc/settings/v2/settings_converter.go b/internal/api/grpc/settings/v2/settings_converter.go new file mode 100644 index 0000000000..42ba4767c4 --- /dev/null +++ b/internal/api/grpc/settings/v2/settings_converter.go @@ -0,0 +1,189 @@ +package settings + +import ( + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha" +) + +// TODO: ? +func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings { + multi := make([]settings.MultiFactorType, len(current.MultiFactors)) + for i, typ := range current.MultiFactors { + multi[i] = multiFactorTypeToPb(typ) + } + second := make([]settings.SecondFactorType, len(current.SecondFactors)) + for i, typ := range current.SecondFactors { + second[i] = secondFactorTypeToPb(typ) + } + + return &settings.LoginSettings{ + AllowUsernamePassword: current.AllowUsernamePassword, + AllowRegister: current.AllowRegister, + AllowExternalIdp: current.AllowExternalIDPs, + ForceMfa: current.ForceMFA, + PasskeysType: passkeysTypeToPb(current.PasswordlessType), + HidePasswordReset: current.HidePasswordReset, + IgnoreUnknownUsernames: current.IgnoreUnknownUsernames, + AllowDomainDiscovery: current.AllowDomainDiscovery, + DisableLoginWithEmail: current.DisableLoginWithEmail, + DisableLoginWithPhone: current.DisableLoginWithPhone, + DefaultRedirectUri: current.DefaultRedirectURI, + PasswordCheckLifetime: durationpb.New(current.PasswordCheckLifetime), + ExternalLoginCheckLifetime: durationpb.New(current.ExternalLoginCheckLifetime), + MfaInitSkipLifetime: durationpb.New(current.MFAInitSkipLifetime), + SecondFactorCheckLifetime: durationpb.New(current.SecondFactorCheckLifetime), + MultiFactorCheckLifetime: durationpb.New(current.MultiFactorCheckLifetime), + SecondFactors: second, + MultiFactors: multi, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func isDefaultToResourceOwnerTypePb(isDefault bool) settings.ResourceOwnerType { + if isDefault { + return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE + } + return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG +} + +func passkeysTypeToPb(passwordlessType domain.PasswordlessType) settings.PasskeysType { + switch passwordlessType { + case domain.PasswordlessTypeAllowed: + return settings.PasskeysType_PASSKEYS_TYPE_ALLOWED + case domain.PasswordlessTypeNotAllowed: + return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED + default: + return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED + } +} + +func secondFactorTypeToPb(secondFactorType domain.SecondFactorType) settings.SecondFactorType { + switch secondFactorType { + case domain.SecondFactorTypeOTP: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP + case domain.SecondFactorTypeU2F: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F + case domain.SecondFactorTypeUnspecified: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED + default: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED + } +} + +func multiFactorTypeToPb(typ domain.MultiFactorType) settings.MultiFactorType { + switch typ { + case domain.MultiFactorTypeU2FWithPIN: + return settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION + case domain.MultiFactorTypeUnspecified: + return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED + default: + return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED + } +} + +func passwordSettingsToPb(current *query.PasswordComplexityPolicy) *settings.PasswordComplexitySettings { + return &settings.PasswordComplexitySettings{ + MinLength: current.MinLength, + RequiresUppercase: current.HasUppercase, + RequiresLowercase: current.HasLowercase, + RequiresNumber: current.HasNumber, + RequiresSymbol: current.HasSymbol, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func brandingSettingsToPb(current *query.LabelPolicy, assetPrefix string) *settings.BrandingSettings { + return &settings.BrandingSettings{ + LightTheme: themeToPb(current.Light, assetPrefix, current.ResourceOwner), + DarkTheme: themeToPb(current.Dark, assetPrefix, current.ResourceOwner), + FontUrl: domain.AssetURL(assetPrefix, current.ResourceOwner, current.FontURL), + DisableWatermark: current.WatermarkDisabled, + HideLoginNameSuffix: current.HideLoginNameSuffix, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func themeToPb(theme query.Theme, assetPrefix, resourceOwner string) *settings.Theme { + return &settings.Theme{ + PrimaryColor: theme.PrimaryColor, + BackgroundColor: theme.BackgroundColor, + FontColor: theme.FontColor, + WarnColor: theme.WarnColor, + LogoUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.LogoURL), + IconUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.IconURL), + } +} + +func domainSettingsToPb(current *query.DomainPolicy) *settings.DomainSettings { + return &settings.DomainSettings{ + LoginNameIncludesDomain: current.UserLoginMustBeDomain, + RequireOrgDomainVerification: current.ValidateOrgDomains, + SmtpSenderAddressMatchesInstanceDomain: current.SMTPSenderAddressMatchesInstanceDomain, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func legalAndSupportSettingsToPb(current *query.PrivacyPolicy) *settings.LegalAndSupportSettings { + return &settings.LegalAndSupportSettings{ + TosLink: current.TOSLink, + PrivacyPolicyLink: current.PrivacyLink, + HelpLink: current.HelpLink, + SupportEmail: string(current.SupportEmail), + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func lockoutSettingsToPb(current *query.LockoutPolicy) *settings.LockoutSettings { + return &settings.LockoutSettings{ + MaxPasswordAttempts: current.MaxPasswordAttempts, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func identityProvidersToPb(idps []*query.IDPLoginPolicyLink) []*settings.IdentityProvider { + providers := make([]*settings.IdentityProvider, len(idps)) + for i, idp := range idps { + providers[i] = identityProviderToPb(idp) + } + return providers +} + +func identityProviderToPb(idp *query.IDPLoginPolicyLink) *settings.IdentityProvider { + return &settings.IdentityProvider{ + Id: idp.IDPID, + Name: domain.IDPName(idp.IDPName, idp.IDPType), + Type: idpTypeToPb(idp.IDPType), + } +} + +func idpTypeToPb(idpType domain.IDPType) settings.IdentityProviderType { + switch idpType { + case domain.IDPTypeUnspecified: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED + case domain.IDPTypeOIDC: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC + case domain.IDPTypeJWT: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT + case domain.IDPTypeOAuth: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH + case domain.IDPTypeLDAP: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP + case domain.IDPTypeAzureAD: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD + case domain.IDPTypeGitHub: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB + case domain.IDPTypeGitHubEnterprise: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES + case domain.IDPTypeGitLab: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB + case domain.IDPTypeGitLabSelfHosted: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED + case domain.IDPTypeGoogle: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE + default: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED + } +} diff --git a/internal/api/grpc/settings/v2/settings_converter_test.go b/internal/api/grpc/settings/v2/settings_converter_test.go new file mode 100644 index 0000000000..818bf64ffe --- /dev/null +++ b/internal/api/grpc/settings/v2/settings_converter_test.go @@ -0,0 +1,461 @@ +package settings + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha" +) + +var ignoreMessageTypes = map[protoreflect.FullName]bool{ + "google.protobuf.Duration": true, +} + +// allFieldsSet recusively checks if all values in a message +// have a non-zero value. +func allFieldsSet(t testing.TB, msg protoreflect.Message) { + md := msg.Descriptor() + name := md.FullName() + if ignoreMessageTypes[name] { + return + } + + fields := md.Fields() + + for i := 0; i < fields.Len(); i++ { + fd := fields.Get(i) + if !msg.Has(fd) { + t.Errorf("not all fields set in %q, missing %q", name, fd.Name()) + continue + } + + if fd.Kind() == protoreflect.MessageKind { + allFieldsSet(t, msg.Get(fd).Message()) + } + } +} + +func Test_loginSettingsToPb(t *testing.T) { + arg := &query.LoginPolicy{ + AllowUsernamePassword: true, + AllowRegister: true, + AllowExternalIDPs: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + HidePasswordReset: true, + IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, + DefaultRedirectURI: "example.com", + PasswordCheckLifetime: time.Hour, + ExternalLoginCheckLifetime: time.Minute, + MFAInitSkipLifetime: time.Millisecond, + SecondFactorCheckLifetime: time.Microsecond, + MultiFactorCheckLifetime: time.Nanosecond, + SecondFactors: []domain.SecondFactorType{ + domain.SecondFactorTypeOTP, + domain.SecondFactorTypeU2F, + }, + MultiFactors: []domain.MultiFactorType{ + domain.MultiFactorTypeU2FWithPIN, + }, + IsDefault: true, + } + + want := &settings.LoginSettings{ + AllowUsernamePassword: true, + AllowRegister: true, + AllowExternalIdp: true, + ForceMfa: true, + PasskeysType: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED, + HidePasswordReset: true, + IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, + DefaultRedirectUri: "example.com", + PasswordCheckLifetime: durationpb.New(time.Hour), + ExternalLoginCheckLifetime: durationpb.New(time.Minute), + MfaInitSkipLifetime: durationpb.New(time.Millisecond), + SecondFactorCheckLifetime: durationpb.New(time.Microsecond), + MultiFactorCheckLifetime: durationpb.New(time.Nanosecond), + SecondFactors: []settings.SecondFactorType{ + settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP, + settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F, + }, + MultiFactors: []settings.MultiFactorType{ + settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + + got := loginSettingsToPb(arg) + allFieldsSet(t, got.ProtoReflect()) + if !proto.Equal(got, want) { + t.Errorf("loginSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_isDefaultToResourceOwnerTypePb(t *testing.T) { + type args struct { + isDefault bool + } + tests := []struct { + args args + want settings.ResourceOwnerType + }{ + { + args: args{false}, + want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG, + }, + { + args: args{true}, + want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := isDefaultToResourceOwnerTypePb(tt.args.isDefault) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_passkeysTypeToPb(t *testing.T) { + type args struct { + passwordlessType domain.PasswordlessType + } + tests := []struct { + args args + want settings.PasskeysType + }{ + { + args: args{domain.PasswordlessTypeNotAllowed}, + want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED, + }, + { + args: args{domain.PasswordlessTypeAllowed}, + want: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED, + }, + { + args: args{99}, + want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := passkeysTypeToPb(tt.args.passwordlessType) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_secondFactorTypeToPb(t *testing.T) { + type args struct { + secondFactorType domain.SecondFactorType + } + tests := []struct { + args args + want settings.SecondFactorType + }{ + { + args: args{domain.SecondFactorTypeOTP}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP, + }, + { + args: args{domain.SecondFactorTypeU2F}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F, + }, + { + args: args{domain.SecondFactorTypeUnspecified}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED, + }, + { + args: args{99}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := secondFactorTypeToPb(tt.args.secondFactorType) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_multiFactorTypeToPb(t *testing.T) { + type args struct { + typ domain.MultiFactorType + } + tests := []struct { + args args + want settings.MultiFactorType + }{ + { + args: args{domain.MultiFactorTypeU2FWithPIN}, + want: settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }, + { + args: args{domain.MultiFactorTypeUnspecified}, + want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED, + }, + { + args: args{99}, + want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := multiFactorTypeToPb(tt.args.typ) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_passwordSettingsToPb(t *testing.T) { + arg := &query.PasswordComplexityPolicy{ + MinLength: 12, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + IsDefault: true, + } + want := &settings.PasswordComplexitySettings{ + MinLength: 12, + RequiresUppercase: true, + RequiresLowercase: true, + RequiresNumber: true, + RequiresSymbol: true, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + + got := passwordSettingsToPb(arg) + allFieldsSet(t, got.ProtoReflect()) + if !proto.Equal(got, want) { + t.Errorf("passwordSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_brandingSettingsToPb(t *testing.T) { + arg := &query.LabelPolicy{ + Light: query.Theme{ + PrimaryColor: "red", + WarnColor: "white", + BackgroundColor: "blue", + FontColor: "orange", + LogoURL: "light-logo", + IconURL: "light-icon", + }, + Dark: query.Theme{ + PrimaryColor: "magenta", + WarnColor: "pink", + BackgroundColor: "black", + FontColor: "white", + LogoURL: "dark-logo", + IconURL: "dark-icon", + }, + ResourceOwner: "me", + FontURL: "fonts", + WatermarkDisabled: true, + HideLoginNameSuffix: true, + IsDefault: true, + } + want := &settings.BrandingSettings{ + LightTheme: &settings.Theme{ + PrimaryColor: "red", + WarnColor: "white", + BackgroundColor: "blue", + FontColor: "orange", + LogoUrl: "http://example.com/me/light-logo", + IconUrl: "http://example.com/me/light-icon", + }, + DarkTheme: &settings.Theme{ + PrimaryColor: "magenta", + WarnColor: "pink", + BackgroundColor: "black", + FontColor: "white", + LogoUrl: "http://example.com/me/dark-logo", + IconUrl: "http://example.com/me/dark-icon", + }, + FontUrl: "http://example.com/me/fonts", + DisableWatermark: true, + HideLoginNameSuffix: true, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + + got := brandingSettingsToPb(arg, "http://example.com") + allFieldsSet(t, got.ProtoReflect()) + if !proto.Equal(got, want) { + t.Errorf("brandingSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_domainSettingsToPb(t *testing.T) { + arg := &query.DomainPolicy{ + UserLoginMustBeDomain: true, + ValidateOrgDomains: true, + SMTPSenderAddressMatchesInstanceDomain: true, + IsDefault: true, + } + want := &settings.DomainSettings{ + LoginNameIncludesDomain: true, + RequireOrgDomainVerification: true, + SmtpSenderAddressMatchesInstanceDomain: true, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + got := domainSettingsToPb(arg) + allFieldsSet(t, got.ProtoReflect()) + if !proto.Equal(got, want) { + t.Errorf("domainSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_legalSettingsToPb(t *testing.T) { + arg := &query.PrivacyPolicy{ + TOSLink: "http://example.com/tos", + PrivacyLink: "http://example.com/pricacy", + HelpLink: "http://example.com/help", + SupportEmail: "support@zitadel.com", + IsDefault: true, + } + want := &settings.LegalAndSupportSettings{ + TosLink: "http://example.com/tos", + PrivacyPolicyLink: "http://example.com/pricacy", + HelpLink: "http://example.com/help", + SupportEmail: "support@zitadel.com", + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + got := legalAndSupportSettingsToPb(arg) + allFieldsSet(t, got.ProtoReflect()) + if !proto.Equal(got, want) { + t.Errorf("legalSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_lockoutSettingsToPb(t *testing.T) { + arg := &query.LockoutPolicy{ + MaxPasswordAttempts: 22, + IsDefault: true, + } + want := &settings.LockoutSettings{ + MaxPasswordAttempts: 22, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + got := lockoutSettingsToPb(arg) + allFieldsSet(t, got.ProtoReflect()) + if !proto.Equal(got, want) { + t.Errorf("lockoutSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_identityProvidersToPb(t *testing.T) { + arg := []*query.IDPLoginPolicyLink{ + { + IDPID: "1", + IDPName: "foo", + IDPType: domain.IDPTypeOIDC, + }, + { + IDPID: "2", + IDPName: "bar", + IDPType: domain.IDPTypeGitHub, + }, + } + want := []*settings.IdentityProvider{ + { + Id: "1", + Name: "foo", + Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC, + }, + { + Id: "2", + Name: "bar", + Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB, + }, + } + got := identityProvidersToPb(arg) + require.Len(t, got, len(got)) + for i, v := range got { + allFieldsSet(t, v.ProtoReflect()) + if !proto.Equal(v, want[i]) { + t.Errorf("identityProvidersToPb() =\n%v\nwant\n%v", got, want) + } + } +} + +func Test_idpTypeToPb(t *testing.T) { + type args struct { + idpType domain.IDPType + } + tests := []struct { + args args + want settings.IdentityProviderType + }{ + { + args: args{domain.IDPTypeUnspecified}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED, + }, + { + args: args{domain.IDPTypeOIDC}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC, + }, + { + args: args{domain.IDPTypeJWT}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT, + }, + { + args: args{domain.IDPTypeOAuth}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH, + }, + { + args: args{domain.IDPTypeLDAP}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP, + }, + { + args: args{domain.IDPTypeAzureAD}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD, + }, + { + args: args{domain.IDPTypeGitHub}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB, + }, + { + args: args{domain.IDPTypeGitHubEnterprise}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES, + }, + { + args: args{domain.IDPTypeGitLab}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB, + }, + { + args: args{domain.IDPTypeGitLabSelfHosted}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED, + }, + { + args: args{domain.IDPTypeGoogle}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE, + }, + { + args: args{99}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + if got := idpTypeToPb(tt.args.idpType); !reflect.DeepEqual(got, tt.want) { + t.Errorf("idpTypeToPb() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/domain/idp.go b/internal/domain/idp.go index 5e4959d182..fc0e8b91fe 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -1,5 +1,7 @@ package domain +import "github.com/zitadel/logging" + type IDPState int32 const ( @@ -56,3 +58,36 @@ func (t IDPType) GetCSSClass() string { return "" } } + +func IDPName(name string, idpType IDPType) string { + if name != "" { + return name + } + return idpType.DisplayName() +} + +// DisplayName returns the name or a default +// to be used when always a name must be displayed (e.g. login) +func (t IDPType) DisplayName() string { + switch t { + case IDPTypeGitHub: + return "GitHub" + case IDPTypeGitLab: + return "GitLab" + case IDPTypeGoogle: + return "Google" + case IDPTypeUnspecified, + IDPTypeOIDC, + IDPTypeJWT, + IDPTypeOAuth, + IDPTypeLDAP, + IDPTypeAzureAD, + IDPTypeGitHubEnterprise, + IDPTypeGitLabSelfHosted: + fallthrough + default: + // we should never get here, so log it + logging.Errorf("name of provider (type %d) is empty", t) + return "" + } +} diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go index b8019974e1..b9c169193c 100644 --- a/internal/domain/policy_login.go +++ b/internal/domain/policy_login.go @@ -4,8 +4,6 @@ import ( "net/url" "time" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -70,30 +68,7 @@ func (p IDPProvider) IsValid() bool { // DisplayName returns the name or a default // to be used when always a name must be displayed (e.g. login) func (p IDPProvider) DisplayName() string { - if p.Name != "" { - return p.Name - } - switch p.IDPType { - case IDPTypeGitHub: - return "GitHub" - case IDPTypeGitLab: - return "GitLab" - case IDPTypeGoogle: - return "Google" - case IDPTypeUnspecified, - IDPTypeOIDC, - IDPTypeJWT, - IDPTypeOAuth, - IDPTypeLDAP, - IDPTypeAzureAD, - IDPTypeGitHubEnterprise, - IDPTypeGitLabSelfHosted: - fallthrough - default: - // we should never get here, so log it - logging.Errorf("name of provider (type %d) is empty - id: %s", p.IDPType, p.IDPConfigID) - return "" - } + return IDPName(p.Name, p.IDPType) } type PasswordlessType int32 diff --git a/internal/query/idp_login_policy_link.go b/internal/query/idp_login_policy_link.go index afa5a8f6e8..750f2f1141 100644 --- a/internal/query/idp_login_policy_link.go +++ b/internal/query/idp_login_policy_link.go @@ -80,27 +80,33 @@ var ( name: projection.IDPLoginPolicyLinkOwnerRemovedCol, table: idpLoginPolicyLinkTable, } + + idpLoginPolicyOwnerTable = loginPolicyTable.setAlias("login_policy_owner") + idpLoginPolicyOwnerIDCol = LoginPolicyColumnOrgID.setTable(idpLoginPolicyOwnerTable) + idpLoginPolicyOwnerInstanceIDCol = LoginPolicyColumnInstanceID.setTable(idpLoginPolicyOwnerTable) + idpLoginPolicyOwnerIsDefaultCol = LoginPolicyColumnIsDefault.setTable(idpLoginPolicyOwnerTable) + idpLoginPolicyOwnerOwnerRemovedCol = LoginPolicyColumnOwnerRemoved.setTable(idpLoginPolicyOwnerTable) ) func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, queries *IDPLoginPolicyLinksSearchQuery, withOwnerRemoved bool) (idps *IDPLoginPolicyLinks, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPLoginPolicyLinksQuery(ctx, q.client) + query, scan := prepareIDPLoginPolicyLinksQuery(ctx, q.client, resourceOwner) eq := sq.Eq{ - IDPLoginPolicyLinkResourceOwnerCol.identifier(): resourceOwner, - IDPLoginPolicyLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + IDPLoginPolicyLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } if !withOwnerRemoved { eq[IDPLoginPolicyLinkOwnerRemovedCol.identifier()] = false + eq[idpLoginPolicyOwnerOwnerRemovedCol.identifier()] = false } + stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, errors.ThrowInvalidArgument(err, "QUERY-FDbKW", "Errors.Query.InvalidRequest") } - rows, err := q.client.QueryContext(ctx, stmt, args...) - if err != nil { + if err != nil || rows.Err() != nil { return nil, errors.ThrowInternal(err, "QUERY-ZkKUc", "Errors.Internal") } idps, err = scan(rows) @@ -111,7 +117,11 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, return idps, err } -func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { +func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + resourceOwnerQuery, resourceOwnerArgs, err := prepareIDPLoginPolicyLinksResourceOwnerQuery(ctx, resourceOwner) + if err != nil { + return sq.SelectBuilder{}, nil + } return sq.Select( IDPLoginPolicyLinkIDPIDCol.identifier(), IDPTemplateNameCol.identifier(), @@ -119,7 +129,12 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (s IDPTemplateOwnerTypeCol.identifier(), countColumn.identifier()). From(idpLoginPolicyLinkTable.identifier()). - LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol)). + RightJoin("("+resourceOwnerQuery+") AS "+idpLoginPolicyOwnerTable.alias+" ON "+ + idpLoginPolicyOwnerIDCol.identifier()+" = "+IDPLoginPolicyLinkResourceOwnerCol.identifier()+" AND "+ + idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier()+ + " "+db.Timetravel(call.Took(ctx)), + resourceOwnerArgs...). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPLoginPolicyLinks, error) { links := make([]*IDPLoginPolicyLink, 0) @@ -164,3 +179,22 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (s }, nil } } + +func prepareIDPLoginPolicyLinksResourceOwnerQuery(ctx context.Context, resourceOwner string) (string, []interface{}, error) { + eqPolicy := sq.Eq{idpLoginPolicyOwnerInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} + return sq.Select( + idpLoginPolicyOwnerIDCol.identifier(), + idpLoginPolicyOwnerInstanceIDCol.identifier(), + idpLoginPolicyOwnerOwnerRemovedCol.identifier(), + ). + From(idpLoginPolicyOwnerTable.identifier()). + Where( + sq.And{ + eqPolicy, + sq.Or{ + sq.Eq{idpLoginPolicyOwnerIDCol.identifier(): resourceOwner}, + sq.Eq{idpLoginPolicyOwnerIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}, + }, + }). + Limit(1).OrderBy(idpLoginPolicyOwnerIsDefaultCol.identifier()).ToSql() +} diff --git a/internal/query/idp_login_policy_link_test.go b/internal/query/idp_login_policy_link_test.go index 4e802d9542..530780160c 100644 --- a/internal/query/idp_login_policy_link_test.go +++ b/internal/query/idp_login_policy_link_test.go @@ -1,6 +1,7 @@ package query import ( + "context" "database/sql" "database/sql/driver" "errors" @@ -8,6 +9,8 @@ import ( "regexp" "testing" + sq "github.com/Masterminds/squirrel" + "github.com/zitadel/zitadel/internal/domain" ) @@ -19,6 +22,9 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.idp_login_policy_links5` + ` LEFT JOIN projections.idp_templates5 ON projections.idp_login_policy_links5.idp_id = projections.idp_templates5.id AND projections.idp_login_policy_links5.instance_id = projections.idp_templates5.instance_id` + + ` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies4 AS login_policy_owner` + + ` WHERE (login_policy_owner.instance_id = $1 AND (login_policy_owner.aggregate_id = $2 OR login_policy_owner.aggregate_id = $3)) ORDER BY login_policy_owner.is_default LIMIT 1) AS login_policy_owner` + + ` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) loginPolicyIDPLinksCols = []string{ "idp_id", @@ -41,8 +47,10 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { object interface{} }{ { - name: "prepareIDPsQuery found", - prepare: prepareIDPLoginPolicyLinksQuery, + name: "prepareIDPsQuery found", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + }, want: want{ sqlExpectations: mockQueries( loginPolicyIDPLinksQuery, @@ -72,8 +80,10 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, }, { - name: "prepareIDPsQuery no idp", - prepare: prepareIDPLoginPolicyLinksQuery, + name: "prepareIDPsQuery no idp", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + }, want: want{ sqlExpectations: mockQueries( loginPolicyIDPLinksQuery, @@ -102,8 +112,10 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, }, { - name: "prepareIDPsQuery sql err", - prepare: prepareIDPLoginPolicyLinksQuery, + name: "prepareIDPsQuery sql err", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + }, want: want{ sqlExpectations: mockQueryErr( loginPolicyIDPLinksQuery, diff --git a/proto/zitadel/object/v2alpha/object.proto b/proto/zitadel/object/v2alpha/object.proto index 27c6dbb395..5150ad7929 100644 --- a/proto/zitadel/object/v2alpha/object.proto +++ b/proto/zitadel/object/v2alpha/object.proto @@ -15,6 +15,13 @@ message Organisation { } } +message RequestContext { + oneof resource_owner { + string org_id = 1; + bool instance = 2 [(validate.rules).bool = {const: true}]; + } +} + message ListQuery { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { diff --git a/proto/zitadel/settings/v2alpha/branding_settings.proto b/proto/zitadel/settings/v2alpha/branding_settings.proto new file mode 100644 index 0000000000..a98b67769a --- /dev/null +++ b/proto/zitadel/settings/v2alpha/branding_settings.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2alpha/settings.proto"; + +message BrandingSettings { + Theme light_theme = 1; + Theme dark_theme = 2; + string font_url = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "url to the font used"; + example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/font-180950243237405441\""; + } + ]; + // hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set + bool hide_login_name_suffix = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set"; + } + ]; + bool disable_watermark = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "boolean to disable the watermark"; + } + ]; + // resource_owner_type returns if the setting is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the setting is managed on the organization or on the instance"; + } + ]; +} + +message Theme { + // hex value for primary color + string primary_color = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for primary color"; + example: "\"#5469d4\""; + } + ]; + // hex value for background color + string background_color = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for background color"; + example: "\"#FAFAFA\""; + } + ]; + // hex value for warning color + string warn_color = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for warn color"; + example: "\"#CD3D56\""; + } + ]; + // hex value for font color + string font_color = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for font color"; + example: "\"#000000\""; + } + ]; + // url where the logo is served + string logo_url = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "url to the logo"; + example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/logo-180950416321494657\""; + } + ]; + // url where the icon is served + string icon_url = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "url to the icon"; + example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/icon-180950498874178817\""; + } + ]; +} diff --git a/proto/zitadel/settings/v2alpha/domain_settings.proto b/proto/zitadel/settings/v2alpha/domain_settings.proto new file mode 100644 index 0000000000..4eae13dc70 --- /dev/null +++ b/proto/zitadel/settings/v2alpha/domain_settings.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2alpha/settings.proto"; + +message DomainSettings { + bool login_name_includes_domain = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the username has to end with the domain of its organization" + } + ]; + bool require_org_domain_verification = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if organization domains should be verified upon creation, otherwise will be created already verified" + } + ]; + bool smtp_sender_address_matches_instance_domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the SMTP sender address domain should match an existing domain on the instance" + } + ]; + // resource_owner_type returns if the setting is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the setting is managed on the organization or on the instance"; + } + ]; +} + diff --git a/proto/zitadel/settings/v2alpha/legal_settings.proto b/proto/zitadel/settings/v2alpha/legal_settings.proto new file mode 100644 index 0000000000..3b9d5694e2 --- /dev/null +++ b/proto/zitadel/settings/v2alpha/legal_settings.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2alpha/settings.proto"; +import "validate/validate.proto"; + +message LegalAndSupportSettings { + string tos_link = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://zitadel.com/docs/legal/terms-of-service\""; + } + ]; + string privacy_policy_link = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://zitadel.com/docs/legal/privacy-policy\""; + } + ]; + string help_link = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://zitadel.com/docs/manuals/introduction\""; + } + ]; + string support_email = 4 [ + (validate.rules).string = {ignore_empty: true, max_len: 320, email: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"support-email@test.com\""; + description: "help / support email address." + } + ]; + // resource_owner_type returns if the setting is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the setting is managed on the organization or on the instance"; + } + ]; +} diff --git a/proto/zitadel/settings/v2alpha/lockout_settings.proto b/proto/zitadel/settings/v2alpha/lockout_settings.proto new file mode 100644 index 0000000000..7a5ad6244e --- /dev/null +++ b/proto/zitadel/settings/v2alpha/lockout_settings.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2alpha/settings.proto"; + +message LockoutSettings { + uint64 max_password_attempts = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Maximum password check attempts before the account gets locked. Attempts are reset as soon as the password is entered correctly or the password is reset. If set to 0 the account will never be locked." + example: "\"10\"" + } + ]; + // resource_owner_type returns if the settings is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; + } + ]; +} diff --git a/proto/zitadel/settings/v2alpha/login_settings.proto b/proto/zitadel/settings/v2alpha/login_settings.proto new file mode 100644 index 0000000000..d84423c3cf --- /dev/null +++ b/proto/zitadel/settings/v2alpha/login_settings.proto @@ -0,0 +1,143 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2alpha/settings.proto"; +import "google/protobuf/duration.proto"; + +message LoginSettings { + bool allow_username_password = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a user is allowed to log in with his username and password"; + } + ]; + bool allow_register = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a person is allowed to register a user on this organization"; + } + ]; + bool allow_external_idp = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a user is allowed to add a defined identity provider. E.g. Google auth"; + } + ]; + bool force_mfa = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a user MUST use a multi-factor to log in"; + } + ]; + PasskeysType passkeys_type = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if passkeys are allowed for users" + } + ]; + bool hide_password_reset = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if password reset link should be shown in the login screen" + } + ]; + bool ignore_unknown_usernames = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if unknown username on login screen directly returns an error or always displays the password screen" + } + ]; + string default_redirect_uri = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines where the user will be redirected to if the login is started without app context (e.g. from mail)"; + example: "\"https://acme.com/ui/console\""; + } + ]; + google.protobuf.Duration password_check_lifetime = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how much time the user has to re-authenticate with the password."; + example: "\"864000s\""; + } + ]; + google.protobuf.Duration external_login_check_lifetime = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how much time the user has to re-authenticate with an external provider."; + example: "\"864000s\""; + } + ]; + google.protobuf.Duration mfa_init_skip_lifetime = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how much time the mfa prompt will be shown again."; + example: "\"2592000s\""; + } + ]; + google.protobuf.Duration second_factor_check_lifetime = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how long the second-factor check is valid."; + example: "\"64800s\""; + } + ]; + google.protobuf.Duration multi_factor_check_lifetime = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines how long the multi-factor check is valid."; + example: "\"43200s\""; + } + ]; + repeated SecondFactorType second_factors = 14; + repeated MultiFactorType multi_factors = 15; + // If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organization on success. + bool allow_domain_discovery = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organization on success." + } + ]; + bool disable_login_with_email = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the user can additionally (to the login name) be identified by their verified email address" + } + ]; + bool disable_login_with_phone = 18 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the user can additionally (to the login name) be identified by their verified phone number" + } + ]; + // resource_owner_type returns if the settings is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 19 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; + } + ]; +} + +enum SecondFactorType { + SECOND_FACTOR_TYPE_UNSPECIFIED = 0; + SECOND_FACTOR_TYPE_OTP = 1; + SECOND_FACTOR_TYPE_U2F = 2; +} + +enum MultiFactorType { + MULTI_FACTOR_TYPE_UNSPECIFIED = 0; + MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION = 1; +} + +enum PasskeysType { + PASSKEYS_TYPE_NOT_ALLOWED = 0; + PASSKEYS_TYPE_ALLOWED = 1; +} + +message IdentityProvider { + string id = 1; + string name = 2; + IdentityProviderType type = 3; +} + +enum IdentityProviderType { + IDENTITY_PROVIDER_TYPE_UNSPECIFIED = 0; + IDENTITY_PROVIDER_TYPE_OIDC = 1; + IDENTITY_PROVIDER_TYPE_JWT = 2; + IDENTITY_PROVIDER_TYPE_LDAP = 3; + IDENTITY_PROVIDER_TYPE_OAUTH = 4; + IDENTITY_PROVIDER_TYPE_AZURE_AD = 5; + IDENTITY_PROVIDER_TYPE_GITHUB = 6; + IDENTITY_PROVIDER_TYPE_GITHUB_ES = 7; + IDENTITY_PROVIDER_TYPE_GITLAB = 8; + IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9; + IDENTITY_PROVIDER_TYPE_GOOGLE = 10; +} diff --git a/proto/zitadel/settings/v2alpha/password_settings.proto b/proto/zitadel/settings/v2alpha/password_settings.proto new file mode 100644 index 0000000000..f5f30be47d --- /dev/null +++ b/proto/zitadel/settings/v2alpha/password_settings.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2alpha/settings.proto"; + +message PasswordComplexitySettings { + uint64 min_length = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines the minimum length of a password."; + example: "\"8\"" + } + ]; + bool requires_uppercase = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain an upper case letter" + } + ]; + bool requires_lowercase = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain a lowercase letter" + } + ]; + bool requires_number = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain a number" + } + ]; + bool requires_symbol = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain a symbol. E.g. \"$\"" + } + ]; + // resource_owner_type returns if the settings is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; + } + ]; +} diff --git a/proto/zitadel/settings/v2alpha/settings.proto b/proto/zitadel/settings/v2alpha/settings.proto new file mode 100644 index 0000000000..7131015514 --- /dev/null +++ b/proto/zitadel/settings/v2alpha/settings.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +enum ResourceOwnerType { + RESOURCE_OWNER_TYPE_UNSPECIFIED = 0; + RESOURCE_OWNER_TYPE_INSTANCE = 1; + RESOURCE_OWNER_TYPE_ORG = 2; +} diff --git a/proto/zitadel/settings/v2alpha/settings_service.proto b/proto/zitadel/settings/v2alpha/settings_service.proto new file mode 100644 index 0000000000..0c7de35bf8 --- /dev/null +++ b/proto/zitadel/settings/v2alpha/settings_service.proto @@ -0,0 +1,356 @@ +syntax = "proto3"; + +package zitadel.settings.v2alpha; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/object/v2alpha/object.proto"; +import "zitadel/settings/v2alpha/branding_settings.proto"; +import "zitadel/settings/v2alpha/domain_settings.proto"; +import "zitadel/settings/v2alpha/legal_settings.proto"; +import "zitadel/settings/v2alpha/lockout_settings.proto"; +import "zitadel/settings/v2alpha/login_settings.proto"; +import "zitadel/settings/v2alpha/password_settings.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Settings Service"; + version: "2.0-alpha"; + description: "This API is intended to manage settings in a ZITADEL instance. This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login."; + 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: "$ZITADEL_DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + + 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 SettingsService { + + // Get basic information over the instance + rpc GetGeneralSettings (GetGeneralSettingsRequest) returns (GetGeneralSettingsResponse) { + option (google.api.http) = { + get: "/v2alpha/settings" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get basic information over the instance"; + description: "Return the basic information of the instance for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the login settings + rpc GetLoginSettings (GetLoginSettingsRequest) returns (GetLoginSettingsResponse) { + option (google.api.http) = { + get: "/v2alpha/settings/login" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the login settings"; + description: "Return the settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the current active identity providers + rpc GetActiveIdentityProviders (GetActiveIdentityProvidersRequest) returns (GetActiveIdentityProvidersResponse) { + option (google.api.http) = { + get: "/v2alpha/settings/login/idps" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the current active identity providers"; + description: "Return the current active identity providers for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the password complexity settings + rpc GetPasswordComplexitySettings (GetPasswordComplexitySettingsRequest) returns (GetPasswordComplexitySettingsResponse) { + option (google.api.http) = { + get: "/v2alpha/settings/password/complexity" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the password complexity settings"; + description: "Return the password complexity settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the current active branding settings + rpc GetBrandingSettings (GetBrandingSettingsRequest) returns (GetBrandingSettingsResponse) { + option (google.api.http) = { + get: "/v2alpha/settings/branding" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the current active branding settings"; + description: "Return the current active branding settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the domain settings + rpc GetDomainSettings (GetDomainSettingsRequest) returns (GetDomainSettingsResponse) { + option (google.api.http) = { + get: "/v2alpha/settings/domain" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the domain settings"; + description: "Return the domain settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the legal and support settings + rpc GetLegalAndSupportSettings (GetLegalAndSupportSettingsRequest) returns (GetLegalAndSupportSettingsResponse) { + option (google.api.http) = { + get: "/v2alpha/settings/legal_support" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the legal and support settings"; + description: "Return the legal settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the lockout settings + rpc GetLockoutSettings (GetLockoutSettingsRequest) returns (GetLockoutSettingsResponse) { + option (google.api.http) = { + get: "/v2alpha/settings/lockout" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the lockout settings"; + description: "Return the lockout settings for the requested context, which define when a user will be locked" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } +} + +message GetLoginSettingsRequest { + zitadel.object.v2alpha.RequestContext ctx = 1; +} + +message GetLoginSettingsResponse { + zitadel.object.v2alpha.Details details = 1; + zitadel.settings.v2alpha.LoginSettings settings = 2; +} + +message GetPasswordComplexitySettingsRequest { + zitadel.object.v2alpha.RequestContext ctx = 1; +} + +message GetPasswordComplexitySettingsResponse { + zitadel.object.v2alpha.Details details = 1; + zitadel.settings.v2alpha.PasswordComplexitySettings settings = 2; +} + +message GetBrandingSettingsRequest { + zitadel.object.v2alpha.RequestContext ctx = 1; +} + +message GetBrandingSettingsResponse { + zitadel.object.v2alpha.Details details = 1; + zitadel.settings.v2alpha.BrandingSettings settings = 2; +} + +message GetDomainSettingsRequest { + zitadel.object.v2alpha.RequestContext ctx = 1; +} + +message GetDomainSettingsResponse { + zitadel.object.v2alpha.Details details = 1; + zitadel.settings.v2alpha.DomainSettings settings = 2; +} + +message GetLegalAndSupportSettingsRequest { + zitadel.object.v2alpha.RequestContext ctx = 1; +} + +message GetLegalAndSupportSettingsResponse { + zitadel.object.v2alpha.Details details = 1; + zitadel.settings.v2alpha.LegalAndSupportSettings settings = 2; +} + +message GetLockoutSettingsRequest { + zitadel.object.v2alpha.RequestContext ctx = 1; +} + +message GetLockoutSettingsResponse { + zitadel.object.v2alpha.Details details = 1; + zitadel.settings.v2alpha.LockoutSettings settings = 2; +} + +message GetActiveIdentityProvidersRequest { + zitadel.object.v2alpha.RequestContext ctx = 1; +} + +message GetActiveIdentityProvidersResponse { + zitadel.object.v2alpha.ListDetails details = 1; + repeated zitadel.settings.v2alpha.IdentityProvider identity_providers = 2; +} + +message GetGeneralSettingsRequest {} + +message GetGeneralSettingsResponse { + string default_org_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "default organization for the current context" + } + ]; + string default_language = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "default language for the current context" + example: "\"en\"" + } + ]; + repeated string supported_languages = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"en\", \"de\", \"it\"]" + } + ]; +}