feat(api): return allowed_languages (#11553)

# Which Problems Are Solved

While Zitadel provides a possibility to restrict certain languages to be
used, the corresponding list could not be retrieved in the settings
service (v2). This blocked login v2 implementations from respecting the
list and they would always use all available languages.

# How the Problems Are Solved

- Retrieve the list when checking the instance and pass it into the
context.
- Return it as part of the existing `GetGeneralSettingsResponse`
- This allows us to remove an additional query in some other cases /
endpoints.

# Additional Changes

none

# Additional Context

- required for https://github.com/zitadel/zitadel/pull/11372
- backport to v4.x

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Marco A. <[email protected]>
This commit is contained in:
Livio Spring
2026-02-12 08:44:34 +00:00
committed by GitHub
co-authored by Copilot Marco A.
parent ceffb9c536
commit b0609aa861
14 changed files with 65 additions and 20 deletions
+10 -1
View File
@@ -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
}
+4
View File
@@ -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"
}
@@ -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())
+1 -5
View File
@@ -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()
}
@@ -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"
}
+5
View File
@@ -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
}
@@ -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"
}
+1 -5
View File
@@ -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()
}
+1 -5
View File
@@ -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)
+4
View File
@@ -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"
}
+9
View File
@@ -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
}
}
+4 -2
View File
@@ -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;
+3 -1
View File
@@ -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;
@@ -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{}