diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 2639ea1242..9829ce2eb7 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -11,7 +11,10 @@ import ( "github.com/zitadel/zitadel/internal/feature" ) -var emptyInstance = &instance{} +var ( + emptyInstance = &instance{} + _ Instance = (*instance)(nil) +) type Instance interface { InstanceID() string @@ -19,6 +22,7 @@ type Instance interface { ManagementConsoleClientID() string ManagementConsoleApplicationID() string DefaultLanguage() language.Tag + AllowedLanguages() []language.Tag DefaultOrganisationID() string SecurityPolicyAllowedOrigins() []string EnableImpersonation() bool @@ -43,6 +47,7 @@ type instance struct { clientID string orgID string defaultLanguage language.Tag + allowedLanguages []language.Tag features feature.Features executionTargets target.Router } @@ -75,6 +80,10 @@ func (i *instance) DefaultLanguage() language.Tag { return i.defaultLanguage } +func (i *instance) AllowedLanguages() []language.Tag { + return i.allowedLanguages +} + func (i *instance) DefaultOrganisationID() string { return i.orgID } diff --git a/internal/api/authz/instance_test.go b/internal/api/authz/instance_test.go index 303e68a9c5..769bd88610 100644 --- a/internal/api/authz/instance_test.go +++ b/internal/api/authz/instance_test.go @@ -115,6 +115,10 @@ func (m *mockInstance) DefaultLanguage() language.Tag { return language.English } +func (m *mockInstance) AllowedLanguages() []language.Tag { + return []language.Tag{language.English} +} + func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } diff --git a/internal/api/grpc/action/v2/integration_test/query_test.go b/internal/api/grpc/action/v2/integration_test/query_test.go index 2af0b627ba..5276854cc5 100644 --- a/internal/api/grpc/action/v2/integration_test/query_test.go +++ b/internal/api/grpc/action/v2/integration_test/query_test.go @@ -1112,7 +1112,9 @@ func TestServer_ListPublicKeys(t *testing.T) { func assertPublicKeys(ttt *assert.CollectT, want *action.ListPublicKeysResponse, got *action.ListPublicKeysResponse) { assert.EqualExportedValues(ttt, want.Pagination, got.Pagination) - assert.Len(ttt, got.PublicKeys, len(want.PublicKeys)) + if !assert.Len(ttt, got.PublicKeys, len(want.PublicKeys)) { + return + } for i := range want.PublicKeys { assert.Equal(ttt, want.PublicKeys[i].GetKeyId(), got.PublicKeys[i].GetKeyId()) assert.Equal(ttt, want.PublicKeys[i].GetActive(), got.PublicKeys[i].GetActive()) diff --git a/internal/api/grpc/admin/language.go b/internal/api/grpc/admin/language.go index fd6f19f571..f6d2beb31a 100644 --- a/internal/api/grpc/admin/language.go +++ b/internal/api/grpc/admin/language.go @@ -33,11 +33,7 @@ func (s *Server) GetDefaultLanguage(ctx context.Context, _ *admin_pb.GetDefaultL } func (s *Server) GetAllowedLanguages(ctx context.Context, _ *admin_pb.GetAllowedLanguagesRequest) (*admin_pb.GetAllowedLanguagesResponse, error) { - restrictions, err := s.query.GetInstanceRestrictions(ctx) - if err != nil { - return nil, err - } - allowed := restrictions.AllowedLanguages + allowed := authz.GetInstance(ctx).AllowedLanguages() if len(allowed) == 0 { allowed = i18n.SupportedLanguages() } diff --git a/internal/api/grpc/server/middleware/instance_interceptor_test.go b/internal/api/grpc/server/middleware/instance_interceptor_test.go index 7382879e42..a57baada6f 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor_test.go +++ b/internal/api/grpc/server/middleware/instance_interceptor_test.go @@ -281,6 +281,10 @@ func (m *mockInstance) DefaultLanguage() language.Tag { return language.English } +func (m *mockInstance) AllowedLanguages() []language.Tag { + return []language.Tag{language.English} +} + func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } diff --git a/internal/api/grpc/settings/v2/query.go b/internal/api/grpc/settings/v2/query.go index 26f8b1e262..41983a2d6d 100644 --- a/internal/api/grpc/settings/v2/query.go +++ b/internal/api/grpc/settings/v2/query.go @@ -186,10 +186,15 @@ func activeIdentityProvidersToQuery(req *settings.GetActiveIdentityProvidersRequ func (s *Server) GetGeneralSettings(ctx context.Context, _ *connect.Request[settings.GetGeneralSettingsRequest]) (*connect.Response[settings.GetGeneralSettingsResponse], error) { instance := authz.GetInstance(ctx) + allowedLanguages := instance.AllowedLanguages() + if len(allowedLanguages) == 0 { + allowedLanguages = i18n.SupportedLanguages() + } return connect.NewResponse(&settings.GetGeneralSettingsResponse{ SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), DefaultOrgId: instance.DefaultOrganisationID(), DefaultLanguage: instance.DefaultLanguage().String(), + AllowedLanguages: domain.LanguagesToStrings(allowedLanguages), }), nil } diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index 625fc78a74..a3174e4b7c 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -338,6 +338,10 @@ func (m *mockInstance) DefaultLanguage() language.Tag { return language.English } +func (m *mockInstance) AllowedLanguages() []language.Tag { + return []language.Tag{language.English} +} + func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index 08d5a5a1c2..e2a6f3b309 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -120,11 +120,7 @@ func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op. err = oidcError(ctx, err) span.EndWithError(err) }() - restrictions, err := s.query.GetInstanceRestrictions(ctx) - if err != nil { - return nil, op.NewStatusError(oidc.ErrServerError().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("internal server error"), http.StatusInternalServerError) - } - allowedLanguages := restrictions.AllowedLanguages + allowedLanguages := authz.GetInstance(ctx).AllowedLanguages() if len(allowedLanguages) == 0 { allowedLanguages = i18n.SupportedLanguages() } diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index e1b482d8ee..d0238ce6e5 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -433,11 +433,7 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, transl } func (l *Login) getTranslator(ctx context.Context, authReq *domain.AuthRequest) *i18n.Translator { - restrictions, err := l.query.GetInstanceRestrictions(ctx) - if err != nil { - logging.OnError(err).Warn("cannot load instance restrictions to retrieve allowed languages for creating the translator") - } - translator := l.renderer.NewTranslator(ctx, restrictions.AllowedLanguages) + translator := l.renderer.NewTranslator(ctx, authz.GetInstance(ctx).AllowedLanguages()) if authReq != nil { l.addLoginTranslations(translator, authReq.DefaultTranslations) l.addLoginTranslations(translator, authReq.OrgTranslations) diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 2a2a58108d..cd67a86158 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -205,6 +205,10 @@ func (m *mockInstance) DefaultLanguage() language.Tag { return AllowedLanguage } +func (m *mockInstance) AllowedLanguages() []language.Tag { + return []language.Tag{language.English} +} + func (m *mockInstance) DefaultOrganisationID() string { return "defaultOrgID" } diff --git a/internal/query/instance.go b/internal/query/instance.go index ad3c5e4e1e..0614527e4d 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -16,6 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" target_domain "github.com/zitadel/zitadel/internal/execution/target" @@ -472,6 +473,7 @@ type authzInstance struct { ExternalDomains database.TextArray[string] `json:"external_domains,omitempty"` TrustedDomains database.TextArray[string] `json:"trusted_domains,omitempty"` ExecutionTargets target_domain.Router `json:"execution_targets,omitzero"` + AllowedLangs []language.Tag `json:"allowed_langs,omitempty"` } type csp struct { @@ -499,6 +501,10 @@ func (i *authzInstance) DefaultLanguage() language.Tag { return i.DefaultLang } +func (i *authzInstance) AllowedLanguages() []language.Tag { + return i.AllowedLangs +} + func (i *authzInstance) DefaultOrganisationID() string { return i.DefaultOrgID } @@ -566,6 +572,7 @@ func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) { block sql.NullBool features []byte executionTargetsBytes []byte + allowedLanguages database.TextArray[string] ) err := row.Scan( &instance.ID, @@ -583,6 +590,7 @@ func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) { &instance.ExternalDomains, &instance.TrustedDomains, &executionTargetsBytes, + &allowedLanguages, ) if errors.Is(err, sql.ErrNoRows) { return zerrors.ThrowNotFound(nil, "QUERY-1kIjX", "Errors.Instance.NotFound") @@ -611,6 +619,7 @@ func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) { } instance.ExecutionTargets = target_domain.NewRouter(targets) } + instance.AllowedLangs = domain.StringsToLanguages(allowedLanguages) return nil } } diff --git a/internal/query/instance_by_domain.sql b/internal/query/instance_by_domain.sql index c7065c028d..aaf1052e6d 100644 --- a/internal/query/instance_by_domain.sql +++ b/internal/query/instance_by_domain.sql @@ -73,7 +73,8 @@ select f.features, ed.domains as external_domains, td.domains as trusted_domains, - et.execution_targets + et.execution_targets, + r.allowed_languages from domain d join projections.instances i on i.id = d.instance_id left join projections.security_policies2 s on i.id = s.instance_id @@ -81,4 +82,5 @@ left join projections.limits l on i.id = l.instance_id left join features f on i.id = f.instance_id left join external_domains ed on i.id = ed.instance_id left join trusted_domains td on i.id = td.instance_id -left join execution_targets et on i.id = et.instance_id; +left join execution_targets et on i.id = et.instance_id +left join projections.restrictions2 r on i.id = r.instance_id; diff --git a/internal/query/instance_by_id.sql b/internal/query/instance_by_id.sql index 2b9118c59d..c6e8a0a12e 100644 --- a/internal/query/instance_by_id.sql +++ b/internal/query/instance_by_id.sql @@ -63,7 +63,8 @@ select f.features, ed.domains as external_domains, td.domains as trusted_domains, - et.execution_targets + et.execution_targets, + r.allowed_languages from projections.instances i left join projections.security_policies2 s on i.id = s.instance_id left join projections.limits l on i.id = l.instance_id @@ -71,4 +72,5 @@ left join features f on i.id = f.instance_id left join external_domains ed on i.id = ed.instance_id left join trusted_domains td on i.id = td.instance_id left join execution_targets et on i.id = et.instance_id +left join projections.restrictions2 r on i.id = r.instance_id where i.id = $1; diff --git a/proto/zitadel/settings/v2/settings_service.proto b/proto/zitadel/settings/v2/settings_service.proto index 9e191f9f9f..8d5fe8eead 100644 --- a/proto/zitadel/settings/v2/settings_service.proto +++ b/proto/zitadel/settings/v2/settings_service.proto @@ -701,6 +701,8 @@ message GetGeneralSettingsResponse { ]; // The list of supported languages. + // Note that the instance might restrict the languages further + // only allowing a subset of these languages to be used. // The format is a BCP 47 language tag (e.g. "en", "de", "fr-CH"). repeated string supported_languages = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -711,6 +713,16 @@ message GetGeneralSettingsResponse { // The unique identifier of the default organization. // The default organization is used to assign new users to an organization if no other organization is specified. string default_organization_id = 4; + + // The list of allowed languages for the instance. + // This is a subset of the supported languages to be used in the instance + // e.g. for user selection during registration or language detection in the UI. + // The format is a BCP 47 language tag (e.g. "en", "de", "fr-CH"). + repeated string allowed_languages = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"en\", \"de\", \"it\"]" + } + ]; } message GetSecuritySettingsRequest{}