feat: restrict languages (#6931)

* feat: return 404 or 409 if org reg disallowed

* fix: system limit permissions

* feat: add iam limits api

* feat: disallow public org registrations on default instance

* add integration test

* test: integration

* fix test

* docs: describe public org registrations

* avoid updating docs deps

* fix system limits integration test

* silence integration tests

* fix linting

* ignore strange linter complaints

* review

* improve reset properties naming

* redefine the api

* use restrictions aggregate

* test query

* simplify and test projection

* test commands

* fix unit tests

* move integration test

* support restrictions on default instance

* also test GetRestrictions

* self review

* lint

* abstract away resource owner

* fix tests

* configure supported languages

* fix allowed languages

* fix tests

* default lang must not be restricted

* preferred language must be allowed

* change preferred languages

* check languages everywhere

* lint

* test command side

* lint

* add integration test

* add integration test

* restrict supported ui locales

* lint

* lint

* cleanup

* lint

* allow undefined preferred language

* fix integration tests

* update main

* fix env var

* ignore linter

* ignore linter

* improve integration test config

* reduce cognitive complexity

* compile

* check for duplicates

* remove useless restriction checks

* review

* revert restriction renaming

* fix language restrictions

* lint

* generate

* allow custom texts for supported langs for now

* fix tests

* cleanup

* cleanup

* cleanup

* lint

* unsupported preferred lang is allowed

* fix integration test

* finish reverting to old property name

* finish reverting to old property name

* load languages

* refactor(i18n): centralize translators and fs

* lint

* amplify no validations on preferred languages

* fix integration test

* lint

* fix resetting allowed languages

* test unchanged restrictions
This commit is contained in:
Elio Bischof
2023-12-05 12:12:01 +01:00
committed by GitHub
parent 236930f109
commit dd33538c0a
123 changed files with 4133 additions and 2058 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -3,29 +3,23 @@ package admin
import (
"context"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/api/grpc/text"
caos_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) {
langs, err := s.query.Languages(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
return &admin_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
}
func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) {
lang, err := language.Parse(req.Language)
lang, err := domain.ParseLanguage(req.Language)
if err != nil {
return nil, caos_errors.ThrowInvalidArgument(err, "API-39nnf", "Errors.Language.Parse")
return nil, err
}
details, err := s.command.SetDefaultLanguage(ctx, lang)
details, err := s.command.SetDefaultLanguage(ctx, lang[0])
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,19 @@
package admin
import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func selectLanguagesToCommand(languages *admin.SelectLanguages) (tags []language.Tag, err error) {
allowedLanguages := languages.GetList()
if allowedLanguages == nil && languages != nil {
allowedLanguages = make([]string, 0)
}
if allowedLanguages == nil {
return nil, nil
}
return domain.ParseLanguage(allowedLanguages...)
}

View File

@@ -75,7 +75,6 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*
return nil, err
}
human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine
createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
Name: req.Org.Name,
CustomDomain: req.Org.Domain,

View File

@@ -5,11 +5,19 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) {
details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration})
lang, err := selectLanguagesToCommand(req.GetAllowedLanguages())
if err != nil {
return nil, err
}
details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{
DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration,
AllowedLanguages: lang,
})
if err != nil {
return nil, err
}
@@ -26,5 +34,6 @@ func (s *Server) GetRestrictions(ctx context.Context, _ *admin.GetRestrictionsRe
return &admin.GetRestrictionsResponse{
Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner),
DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration,
AllowedLanguages: domain.LanguagesToStrings(restrictions.AllowedLanguages),
}, nil
}

View File

@@ -6,6 +6,7 @@ import (
"bytes"
"context"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"io"
"net/http"
"net/http/cookiejar"
@@ -29,19 +30,25 @@ func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
jar, err := cookiejar.New(nil)
require.NoError(t, err)
browserSession := &http.Client{Jar: jar}
// Default should be allowed
csrfToken := awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
require.NoError(t, err)
awaitDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken)
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
require.NoError(t, err)
awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
var csrfToken string
t.Run("public org registration is allowed by default", func(*testing.T) {
csrfToken = awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
})
t.Run("disallowing public org registration disables the endpoints", func(*testing.T) {
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
require.NoError(t, err)
awaitPubOrgRegDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken)
})
t.Run("allowing public org registration again re-enables the endpoints", func(*testing.T) {
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
require.NoError(t, err)
awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
})
}
// awaitAllowed doesn't accept a CSRF token, as we expected it to always produce a new one
func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string {
csrfToken := awaitGetResponse(t, ctx, client, parsedURL, http.StatusOK)
// awaitPubOrgRegAllowed doesn't accept a CSRF token, as we expected it to always produce a new one
func awaitPubOrgRegAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string {
csrfToken := awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusOK)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken)
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err)
@@ -49,17 +56,17 @@ func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsed
return csrfToken
}
// awaitDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
func awaitDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
awaitGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
// awaitPubOrgRegDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
func awaitPubOrgRegDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken)
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err)
require.True(t, restrictions.DisallowPublicOrgRegistration)
}
// awaitGetResponse cuts the CSRF token from the response body if it exists
func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
// awaitGetSSRGetResponse cuts the CSRF token from the response body if it exists
func awaitGetSSRGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
var csrfToken []byte
await(t, ctx, func() bool {
resp, err := client.Get(parsedURL.String())
@@ -71,7 +78,7 @@ func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, pa
if hasCsrfToken {
csrfToken, _, _ = bytes.Cut(after, []byte(`">`))
}
return resp.StatusCode == expectCode
return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode)
})
return string(csrfToken)
}
@@ -83,24 +90,6 @@ func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Clien
"gorilla.csrf.Token": {csrfToken},
})
require.NoError(t, err)
return resp.StatusCode == expectCode
return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode)
})
}
func await(t *testing.T, ctx context.Context, cb func() bool) {
deadline, ok := ctx.Deadline()
require.True(t, ok, "context must have deadline")
require.Eventuallyf(
t,
func() bool {
defer func() {
require.Nil(t, recover(), "panic in await callback")
}()
return cb()
},
time.Until(deadline),
100*time.Millisecond,
"awaiting successful callback failed",
)
}

View File

@@ -0,0 +1,258 @@
//go:build integration
package admin_test
import (
"context"
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/text"
"github.com/zitadel/zitadel/pkg/grpc/user"
"golang.org/x/text/language"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"io"
"net/http"
"testing"
"time"
)
func TestServer_Restrictions_AllowedLanguages(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel()
var (
defaultAndAllowedLanguage = language.German
supportedLanguagesStr = []string{language.German.String(), language.English.String(), language.Japanese.String()}
disallowedLanguage = language.Spanish
unsupportedLanguage1 = language.Afrikaans
unsupportedLanguage2 = language.Albanian
)
domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(ctx, SystemCTX)
t.Run("assumed defaults are correct", func(tt *testing.T) {
tt.Run("languages are not restricted by default", func(ttt *testing.T) {
restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{})
require.NoError(ttt, err)
require.Len(ttt, restrictions.AllowedLanguages, 0)
})
tt.Run("default language is English by default", func(ttt *testing.T) {
defaultLang, err := Tester.Client.Admin.GetDefaultLanguage(iamOwnerCtx, &admin.GetDefaultLanguageRequest{})
require.NoError(ttt, err)
require.Equal(ttt, language.Make(defaultLang.Language), language.English)
})
tt.Run("the discovery endpoint returns all supported languages", func(ttt *testing.T) {
checkDiscoveryEndpoint(ttt, domain, supportedLanguagesStr, nil)
})
})
t.Run("restricting the default language fails", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: []string{defaultAndAllowedLanguage.String()}}})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
})
t.Run("not defining any restrictions throws an error", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.InvalidArgument, expectStatus.Code())
})
t.Run("setting the default language works", func(tt *testing.T) {
setAndAwaitDefaultLanguage(iamOwnerCtx, tt, defaultAndAllowedLanguage)
})
t.Run("restricting allowed languages works", func(tt *testing.T) {
setAndAwaitAllowedLanguages(iamOwnerCtx, tt, []string{defaultAndAllowedLanguage.String()})
})
t.Run("setting the default language to a disallowed language fails", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetDefaultLanguage(iamOwnerCtx, &admin.SetDefaultLanguageRequest{Language: disallowedLanguage.String()})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
})
t.Run("the list of supported languages includes the disallowed languages", func(tt *testing.T) {
supported, err := Tester.Client.Admin.GetSupportedLanguages(iamOwnerCtx, &admin.GetSupportedLanguagesRequest{})
require.NoError(tt, err)
require.Condition(tt, contains(supported.GetLanguages(), supportedLanguagesStr))
})
t.Run("the disallowed language is not listed in the discovery endpoint", func(tt *testing.T) {
checkDiscoveryEndpoint(tt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
})
t.Run("the login ui is rendered in the default language", func(tt *testing.T) {
checkLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Allgemeine Geschäftsbedingungen und Datenschutz")
})
t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) {
var importedUser *management.ImportHumanUserResponse
tt.Run("import user", func(ttt *testing.T) {
var err error
importedUser, err = importUser(iamOwnerCtx, unsupportedLanguage1)
require.NoError(ttt, err)
})
tt.Run("change user profile", func(ttt *testing.T) {
_, err := Tester.Client.Mgmt.UpdateHumanProfile(iamOwnerCtx, &management.UpdateHumanProfileRequest{
UserId: importedUser.GetUserId(),
FirstName: "hodor",
LastName: "hodor",
NickName: integration.RandString(5),
DisplayName: "hodor",
PreferredLanguage: unsupportedLanguage2.String(),
Gender: user.Gender_GENDER_MALE,
})
require.NoError(ttt, err)
})
})
t.Run("custom texts are only restricted by the supported languages", func(tt *testing.T) {
_, err := Tester.Client.Admin.SetCustomLoginText(iamOwnerCtx, &admin.SetCustomLoginTextsRequest{
Language: disallowedLanguage.String(),
EmailVerificationText: &text.EmailVerificationScreenText{
Description: "hodor",
},
})
assert.NoError(tt, err)
_, err = Tester.Client.Mgmt.SetCustomLoginText(iamOwnerCtx, &management.SetCustomLoginTextsRequest{
Language: disallowedLanguage.String(),
EmailVerificationText: &text.EmailVerificationScreenText{
Description: "hodor",
},
})
assert.NoError(tt, err)
_, err = Tester.Client.Mgmt.SetCustomInitMessageText(iamOwnerCtx, &management.SetCustomInitMessageTextRequest{
Language: disallowedLanguage.String(),
Text: "hodor",
})
assert.NoError(tt, err)
_, err = Tester.Client.Admin.SetDefaultInitMessageText(iamOwnerCtx, &admin.SetDefaultInitMessageTextRequest{
Language: disallowedLanguage.String(),
Text: "hodor",
})
assert.NoError(tt, err)
})
t.Run("allowing all languages works", func(tt *testing.T) {
tt.Run("restricting allowed languages works", func(ttt *testing.T) {
setAndAwaitAllowedLanguages(iamOwnerCtx, ttt, make([]string, 0))
})
})
t.Run("allowing the language makes it usable again", func(tt *testing.T) {
tt.Run("the disallowed language is listed in the discovery endpoint again", func(ttt *testing.T) {
checkDiscoveryEndpoint(ttt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
})
tt.Run("the login ui is rendered in the allowed language", func(ttt *testing.T) {
checkLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Términos y condiciones")
})
})
}
func setAndAwaitAllowedLanguages(ctx context.Context, t *testing.T, selectLanguages []string) {
_, err := Tester.Client.Admin.SetRestrictions(ctx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: selectLanguages}})
require.NoError(t, err)
awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
defer awaitCancel()
await(t, awaitCtx, func() bool {
restrictions, getErr := Tester.Client.Admin.GetRestrictions(awaitCtx, &admin.GetRestrictionsRequest{})
expectLanguages := selectLanguages
if len(selectLanguages) == 0 {
expectLanguages = nil
}
return assert.NoError(NoopAssertionT, getErr) &&
assert.Equal(NoopAssertionT, expectLanguages, restrictions.GetAllowedLanguages())
})
}
func setAndAwaitDefaultLanguage(ctx context.Context, t *testing.T, lang language.Tag) {
_, err := Tester.Client.Admin.SetDefaultLanguage(ctx, &admin.SetDefaultLanguageRequest{Language: lang.String()})
require.NoError(t, err)
awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
defer awaitCancel()
await(t, awaitCtx, func() bool {
defaultLang, getErr := Tester.Client.Admin.GetDefaultLanguage(awaitCtx, &admin.GetDefaultLanguageRequest{})
return assert.NoError(NoopAssertionT, getErr) &&
assert.Equal(NoopAssertionT, lang.String(), defaultLang.GetLanguage())
})
}
func importUser(ctx context.Context, preferredLanguage language.Tag) (*management.ImportHumanUserResponse, error) {
random := integration.RandString(5)
return Tester.Client.Mgmt.ImportHumanUser(ctx, &management.ImportHumanUserRequest{
UserName: "integration-test-user_" + random,
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: "hodor",
LastName: "hodor",
NickName: "hodor",
PreferredLanguage: preferredLanguage.String(),
},
Email: &management.ImportHumanUserRequest_Email{
Email: random + "@hodor.hodor",
IsEmailVerified: true,
},
PasswordChangeRequired: false,
Password: "Password1!",
})
}
func checkDiscoveryEndpoint(t *testing.T, domain string, containsUILocales, notContainsUILocales []string) {
resp, err := http.Get("http://" + domain + ":8080/.well-known/openid-configuration")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
defer func() {
require.NoError(t, resp.Body.Close())
}()
require.NoError(t, err)
doc := struct {
UILocalesSupported []string `json:"ui_locales_supported"`
}{}
require.NoError(t, json.Unmarshal(body, &doc))
if containsUILocales != nil {
assert.Condition(NoopAssertionT, contains(doc.UILocalesSupported, containsUILocales))
}
if notContainsUILocales != nil {
assert.Condition(NoopAssertionT, not(contains(doc.UILocalesSupported, notContainsUILocales)))
}
}
func checkLoginUILanguage(t *testing.T, domain string, acceptLanguage language.Tag, expectLang language.Tag, containsText string) {
req, err := http.NewRequest(http.MethodGet, "http://"+domain+":8080/ui/login/register", nil)
req.Header.Set("Accept-Language", acceptLanguage.String())
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
defer func() {
require.NoError(t, resp.Body.Close())
}()
require.NoError(t, err)
assert.Containsf(t, string(body), containsText, "login ui language is in "+expectLang.String())
}
// We would love to use assert.Contains here, but it doesn't work with slices of strings
func contains(container []string, subset []string) assert.Comparison {
return func() bool {
if subset == nil {
return true
}
for _, str := range subset {
var found bool
for _, containerStr := range container {
if str == containerStr {
found = true
break
}
}
if !found {
return false
}
}
return true
}
}
func not(cmp assert.Comparison) assert.Comparison {
return func() bool {
return !cmp()
}
}

View File

@@ -8,12 +8,17 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
)
var (
AdminCTX, SystemCTX context.Context
Tester *integration.Tester
// NoopAssertionT is useful in combination with assert.Eventuallyf to use testify assertions in a callback
NoopAssertionT = new(noopAssertionT)
)
func TestMain(m *testing.M) {
@@ -30,3 +35,29 @@ func TestMain(m *testing.M) {
return m.Run()
}())
}
func await(t *testing.T, ctx context.Context, cb func() bool) {
deadline, ok := ctx.Deadline()
require.True(t, ok, "context must have deadline")
assert.Eventuallyf(
t,
func() bool {
defer func() {
// Panics are not recovered and don't mark the test as failed, so we need to do that ourselves
require.Nil(t, recover(), "panic in await callback")
}()
return cb()
},
time.Until(deadline),
100*time.Millisecond,
"awaiting successful callback failed",
)
}
var _ assert.TestingT = (*noopAssertionT)(nil)
type noopAssertionT struct{}
func (*noopAssertionT) FailNow() {}
func (*noopAssertionT) Errorf(string, ...interface{}) {}

View File

@@ -2,15 +2,12 @@ package auth
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/api/grpc/text"
auth_pb "github.com/zitadel/zitadel/pkg/grpc/auth"
)
func (s *Server) GetSupportedLanguages(ctx context.Context, req *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
langs, err := s.query.Languages(ctx)
if err != nil {
return nil, err
}
return &auth_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
func (s *Server) GetSupportedLanguages(context.Context, *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
return &auth_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
}

View File

@@ -2,15 +2,12 @@ package management
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/api/grpc/text"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
)
func (s *Server) GetSupportedLanguages(ctx context.Context, req *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
langs, err := s.query.Languages(ctx)
if err != nil {
return nil, err
}
return &mgmt_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
func (s *Server) GetSupportedLanguages(context.Context, *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
return &mgmt_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
}

View File

@@ -220,8 +220,7 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
human := AddHumanUserRequestToAddHuman(req)
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true)
if err != nil {
if err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true); err != nil {
return nil, err
}
return &mgmt_pb.AddHumanUserResponse{

View File

@@ -55,14 +55,14 @@ func TestImport_and_Get(t *testing.T) {
// create unique names.
lastName := strconv.FormatInt(time.Now().Unix(), 10)
userName := strings.Join([]string{firstName, lastName}, "_")
email := strings.Join([]string{userName, "zitadel.com"}, "@")
email := strings.Join([]string{userName, "example.com"}, "@")
res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
UserName: userName,
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: firstName,
LastName: lastName,
PreferredLanguage: language.Afrikaans.String(),
PreferredLanguage: language.Japanese.String(),
Gender: user.Gender_GENDER_DIVERSE,
},
Email: &management.ImportHumanUserRequest_Email{
@@ -82,3 +82,21 @@ func TestImport_and_Get(t *testing.T) {
})
}
}
func TestImport_UnparsablePreferredLanguage(t *testing.T) {
random := integration.RandString(5)
_, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
UserName: random,
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: random,
LastName: random,
PreferredLanguage: "not valid",
Gender: user.Gender_GENDER_DIVERSE,
},
Email: &management.ImportHumanUserRequest_Email{
Email: random + "@example.com",
IsEmailVerified: true,
},
})
require.NoError(t, err)
}

View File

@@ -24,7 +24,7 @@ const (
)
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor {
translator, err := newZitadelTranslator(language.English)
translator, err := i18n.NewZitadelTranslator(language.English)
logging.OnError(err).Panic("unable to get translator")
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return setInstance(ctx, req, info, handler, verifier, headerName, translator, explicitInstanceIdServices...)

View File

@@ -7,6 +7,7 @@ import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/i18n"
_ "github.com/zitadel/zitadel/internal/statik"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
@@ -18,17 +19,15 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc.
defer func() { span.EndWithError(err) }()
if loc, ok := resp.(localizers); ok && resp != nil {
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
translator, translatorError := getTranslator(ctx)
if translatorError != nil {
logging.New().WithError(translatorError).Error("could not load translator")
return resp, err
}
translateFields(ctx, loc, translator)
}
if err != nil {
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
translator, translatorError := getTranslator(ctx)
if translatorError != nil {
logging.New().WithError(translatorError).Error("could not load translator")
return resp, err
}
err = translateError(ctx, err, translator)
@@ -36,3 +35,11 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc.
return resp, err
}
}
func getTranslator(ctx context.Context) (*i18n.Translator, error) {
translator, err := i18n.NewZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
if err != nil {
logging.New().WithError(err).Error("could not load translator")
}
return translator, err
}

View File

@@ -4,10 +4,6 @@ import (
"context"
"errors"
"github.com/rakyll/statik/fs"
"github.com/zitadel/logging"
"golang.org/x/text/language"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/i18n"
)
@@ -39,14 +35,3 @@ func translateError(ctx context.Context, err error, translator *i18n.Translator)
}
return err
}
func newZitadelTranslator(defaultLanguage language.Tag) (*i18n.Translator, error) {
return translatorFromNamespace("zitadel", defaultLanguage)
}
func translatorFromNamespace(namespace string, defaultLanguage language.Tag) (*i18n.Translator, error) {
dir, err := fs.NewWithNamespace(namespace)
logging.WithFields("namespace", namespace).OnError(err).Panic("unable to get namespace")
return i18n.NewTranslator(dir, defaultLanguage, "")
}

View File

@@ -7,7 +7,8 @@ import (
"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/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
@@ -116,13 +117,9 @@ func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.G
}
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),
SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()),
DefaultOrgId: instance.DefaultOrganisationID(),
DefaultLanguage: instance.DefaultLanguage().String(),
}, nil

View File

@@ -1,13 +0,0 @@
package text
import (
"golang.org/x/text/language"
)
func LanguageTagsToStrings(langs []language.Tag) []string {
result := make([]string, len(langs))
for i, lang := range langs {
result[i] = lang.String()
}
return result
}

View File

@@ -28,8 +28,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
return nil, err
}
orgID := authz.GetCtxData(ctx).OrgID
err = s.command.AddHuman(ctx, orgID, human, false)
if err != nil {
if err = s.command.AddHuman(ctx, orgID, human, false); err != nil {
return nil, err
}
return &user.AddHumanUserResponse{

View File

@@ -677,7 +677,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
parametersEqual: map[string]string{
"client_id": "clientID",
"prompt": "select_account",
"redirect_uri": "http://localhost:8080/idps/callback",
"redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback",
"response_type": "code",
"scope": "openid profile email",
},
@@ -704,7 +704,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
url: "http://localhost:8000/sso",
url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,
@@ -728,7 +728,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
url: "http://localhost:8000/sso",
url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,

View File

@@ -8,7 +8,6 @@ import (
"net/url"
"strings"
"github.com/rakyll/statik/fs"
"github.com/zitadel/logging"
"golang.org/x/text/language"
@@ -120,10 +119,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) {
}
func newZitadelTranslator() *i18n.Translator {
dir, err := fs.NewWithNamespace("zitadel")
logging.WithFields("namespace", "zitadel").OnError(err).Panic("unable to get namespace")
translator, err := i18n.NewTranslator(dir, language.English, "")
translator, err := i18n.NewZitadelTranslator(language.English)
logging.OnError(err).Panic("unable to get translator")
return translator
}

View File

@@ -0,0 +1,18 @@
package middleware
import (
"testing"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/i18n"
)
var (
SupportedLanguages = []language.Tag{language.English, language.German}
)
func TestMain(m *testing.M) {
i18n.SupportLanguages(SupportedLanguages...)
m.Run()
}

View File

@@ -6,11 +6,9 @@ import (
"net/http"
"time"
"github.com/rakyll/statik/fs"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"golang.org/x/exp/slog"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/assets"
http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -23,7 +21,6 @@ import (
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/metrics"
)
@@ -167,10 +164,6 @@ func ignoredQuotaLimitEndpoint(endpoints *EndpointConfig) []string {
}
func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) {
supportedLanguages, err := getSupportedLanguages()
if err != nil {
return nil, err
}
opConfig := &op.Config{
DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
CodeMethodS256: config.CodeMethodS256,
@@ -178,7 +171,6 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []
AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT,
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
RequestObjectSupported: config.RequestObjectSupported,
SupportedUILocales: supportedLanguages,
DeviceAuthorization: config.DeviceAuth.toOPConfig(),
}
if cryptoLength := len(cryptoKey); cryptoLength != 32 {
@@ -211,11 +203,3 @@ func newStorage(config Config, command *command.Commands, query *query.Queries,
func (o *OPStorage) Health(ctx context.Context) error {
return o.repo.Health(ctx)
}
func getSupportedLanguages() ([]language.Tag, error) {
statikLoginFS, err := fs.NewWithNamespace("login")
if err != nil {
return nil, err
}
return i18n.SupportedLanguages(statikLoginFS)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/auth/repository"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
@@ -103,8 +104,15 @@ func (s *Server) Ready(ctx context.Context, r *op.Request[struct{}]) (_ *op.Resp
func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return op.NewResponse(s.createDiscoveryConfig(ctx)), nil
restrictions, err := s.query.GetInstanceRestrictions(ctx)
if err != nil {
return nil, err
}
allowedLanguages := restrictions.AllowedLanguages
if len(allowedLanguages) == 0 {
allowedLanguages = i18n.SupportedLanguages()
}
return op.NewResponse(s.createDiscoveryConfig(ctx, allowedLanguages)), nil
}
func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) {
@@ -205,7 +213,7 @@ func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRe
return s.LegacyServer.EndSession(ctx, r)
}
func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfiguration {
func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales oidc.Locales) *oidc.DiscoveryConfiguration {
issuer := op.IssuerFromContext(ctx)
return &oidc.DiscoveryConfiguration{
Issuer: issuer,
@@ -231,7 +239,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfi
RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()),
ClaimsSupported: op.SupportedClaims(s.Provider()),
CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()),
UILocalesSupported: s.Provider().SupportedUILocales(),
UILocalesSupported: supportedUILocales,
RequestParameterSupported: s.Provider().RequestObjectSupported(),
}
}

View File

@@ -16,7 +16,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
signingKeyAlgorithm string
}
type args struct {
ctx context.Context
ctx context.Context
supportedUILocales []language.Tag
}
tests := []struct {
name string
@@ -36,7 +37,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
AuthMethodPrivateKeyJWT: true,
GrantTypeRefreshToken: true,
RequestObjectSupported: true,
SupportedUILocales: []language.Tag{language.English, language.German},
},
nil,
)
@@ -56,7 +56,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
signingKeyAlgorithm: "RS256",
},
args{
ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"),
ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"),
supportedUILocales: []language.Tag{language.English, language.German},
},
&oidc.DiscoveryConfiguration{
Issuer: "https://issuer.com",
@@ -113,7 +114,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
LegacyServer: tt.fields.LegacyServer,
signingKeyAlgorithm: tt.fields.signingKeyAlgorithm,
}
assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx), "createDiscoveryConfig(%v)", tt.args.ctx)
assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx, tt.args.supportedUILocales), "createDiscoveryConfig(%v)", tt.args.ctx)
})
}
}

View File

@@ -36,13 +36,13 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
var errType, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
errType, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := passwordData{
baseData: l.getBaseData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage),
profileData: l.getProfileData(authReq),
}
policy := l.getPasswordComplexityPolicy(r, authReq.UserOrgID)
@@ -65,8 +65,7 @@ func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, aut
}
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
var errType, errMessage string
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage)
data := l.getUserData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", "", "")
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil)
}

View File

@@ -28,13 +28,13 @@ func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request,
logging.WithError(err).Error()
errID, errMessage = l.getErrorMessage(r, err)
}
data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
translator := l.getTranslator(r.Context(), nil)
data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil)
}
func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) {
translator := l.getTranslator(r.Context(), authReq)
data := &struct {
baseData
AuthRequestID string
@@ -42,14 +42,13 @@ func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, a
ClientID string
Scopes []string
}{
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
AuthRequestID: authReq.ID,
Username: authReq.UserName,
ClientID: authReq.ApplicationID,
Scopes: scopes,
}
translator := l.getTranslator(r.Context(), authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil)
}
@@ -60,14 +59,13 @@ const (
// renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied.
func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) {
translator := l.getTranslator(r.Context(), authReq)
data := &struct {
baseData
Message string
}{
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
}
translator := l.getTranslator(r.Context(), authReq)
switch action {
case deviceAuthAllowed:
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)

View File

@@ -549,7 +549,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ
translator := l.getTranslator(r.Context(), authReq)
data := externalNotFoundOptionData{
baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
externalNotFoundOptionFormData: externalNotFoundOptionFormData{
externalRegisterFormData: externalRegisterFormData{
Email: human.EmailAddress,

View File

@@ -122,7 +122,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
translator := l.getTranslator(r.Context(), authReq)
data := initPasswordData{
baseData: l.getBaseData(r, authReq, "InitPassword.Title", "InitPassword.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "InitPassword.Title", "InitPassword.Description", errID, errMessage),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,
@@ -153,8 +153,8 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
}
func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
data := l.getUserData(r, authReq, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
if authReq == nil {
l.customTexts(r.Context(), translator, orgID)
}

View File

@@ -118,7 +118,7 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
translator := l.getTranslator(r.Context(), authReq)
data := initUserData{
baseData: l.getBaseData(r, authReq, "InitUser.Title", "InitUser.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "InitUser.Title", "InitUser.Description", errID, errMessage),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,
@@ -155,8 +155,8 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
}
func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
data := l.getUserData(r, authReq, "InitUserDone.Title", "InitUserDone.Description", "", "")
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", "", "")
if authReq == nil {
l.customTexts(r.Context(), translator, orgID)
}

View File

@@ -35,8 +35,9 @@ func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq
errID, errMessage = l.getErrorMessage(r, err)
}
temp := l.renderer.Templates[tmplLDAPLogin]
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil)
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, translator, temp, data, nil)
}
func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) {

View File

@@ -19,6 +19,7 @@ func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domai
func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errType, errMessage string
data := l.getUserData(r, authReq, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLinkUsersDone], data, nil)
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil)
}

View File

@@ -2,15 +2,12 @@ package login
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"github.com/rakyll/statik/fs"
"github.com/zitadel/zitadel/feature"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -93,17 +90,12 @@ func CreateLogin(config Config,
userCodeAlg: userCodeAlg,
featureCheck: featureCheck,
}
statikFS, err := fs.NewWithNamespace("login")
if err != nil {
return nil, fmt.Errorf("unable to create filesystem: %w", err)
}
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName)
login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName)
login.parser = form.NewParser()
return login, nil
}

View File

@@ -99,7 +99,8 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom
l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID)
return
}
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage)
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
funcs := map[string]interface{}{
"hasUsernamePasswordLogin": func() bool {
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword
@@ -111,7 +112,7 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowRegister
},
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLogin], data, funcs)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogin], data, funcs)
}
func singleIDPAllowed(authReq *domain.AuthRequest) bool {

View File

@@ -41,8 +41,9 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := loginSuccessData{
userData: l.getUserData(r, authReq, "LoginSuccess.Title", "", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", errID, errMessage),
}
if authReq != nil {
data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq)
@@ -51,7 +52,7 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
return
}
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLoginSuccess], data, nil)
}
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {

View File

@@ -13,6 +13,7 @@ func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) {
data := l.getUserData(r, nil, "LogoutDone.Title", "LogoutDone.Description", "", "")
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), nil), l.renderer.Templates[tmplLogoutDone], data, nil)
translator := l.getTranslator(r.Context(), nil)
data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", "", "")
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogoutDone], data, nil)
}

View File

@@ -95,7 +95,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
UserID: userID,
profileData: l.getProfileData(authReq),
}
@@ -111,7 +111,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""),
baseData: l.getBaseData(r, authReq, translator, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""),
profileData: l.getProfileData(authReq),
}
if authReq == nil {

View File

@@ -16,7 +16,7 @@ type mfaInitDoneData struct {
func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) {
var errType, errMessage string
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage)
data.profileData = l.getProfileData(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil)
}

View File

@@ -57,10 +57,11 @@ func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authRe
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.profileData = l.getProfileData(authReq)
data.MFAType = domain.MFATypeOTPSMS
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFASMSInit], data, nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFASMSInit], data, nil)
}
// handleRegisterSMSCheck handles form submissions of the SMS registration.

View File

@@ -29,14 +29,15 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe
if u2f != nil {
credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData)
}
translator := l.getTranslator(r.Context(), authReq)
data := &u2fInitData{
webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage),
CredentialCreationData: credentialData,
},
MFAType: domain.MFATypeU2F,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFAU2FInit], data, nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAU2FInit], data, nil)
}
func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) {

View File

@@ -71,7 +71,7 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.profileData = l.getProfileData(authReq)
if data.MFAType == domain.MFATypeTOTP {
code, err := generateQrCode(data.totpData.Url)

View File

@@ -56,7 +56,7 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq
}
translator := l.getTranslator(r.Context(), authReq)
data := mfaData{
baseData: l.getBaseData(r, authReq, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage),
profileData: l.getProfileData(authReq),
}

View File

@@ -66,12 +66,12 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request,
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := l.getUserData(r, authReq, "", "", errID, errMessage)
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "", "", errID, errMessage)
if verificationStep == nil {
l.renderError(w, r, authReq, err)
return
}
translator := l.getTranslator(r.Context(), authReq)
switch selectedProvider {
case domain.MFATypeU2F:

View File

@@ -61,12 +61,13 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := &mfaOTPData{
userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
MFAProviders: removeSelectedProviderFromList(providers, selectedProvider),
SelectedProvider: selectedProvider,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplOTPVerification], data, nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplOTPVerification], data, nil)
}
// handleOTPVerificationCheck handles form submissions of the OTP verification.

View File

@@ -37,15 +37,16 @@ func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, au
if webAuthNLogin != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
}
translator := l.getTranslator(r.Context(), authReq)
data := &mfaU2FData{
webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
CredentialCreationData: credentialData,
},
MFAProviders: providers,
SelectedProvider: -1,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplU2FVerification], data, nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplU2FVerification], data, nil)
}
func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) {

View File

@@ -19,7 +19,8 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := l.getUserData(r, authReq, "Password.Title", "Password.Description", errID, errMessage)
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Password.Title", "Password.Description", errID, errMessage)
funcs := map[string]interface{}{
"showPasswordReset": func() bool {
if authReq.LoginPolicy != nil {
@@ -28,7 +29,7 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
return true
},
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPassword], data, funcs)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPassword], data, funcs)
}
func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {

View File

@@ -48,6 +48,7 @@ func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request,
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := l.getUserData(r, authReq, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordResetDone], data, nil)
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordResetDone], data, nil)
}

View File

@@ -36,14 +36,15 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re
if passwordSet && authReq.LoginPolicy != nil {
passwordSet = authReq.LoginPolicy.AllowUsernamePassword
}
translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessData{
webAuthNData{
userData: l.getUserData(r, authReq, "Passwordless.Title", "Passwordless.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "Passwordless.Title", "Passwordless.Description", errID, errMessage),
CredentialCreationData: credentialData,
},
passwordSet,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordlessVerification], data, nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessVerification], data, nil)
}
func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) {

View File

@@ -31,10 +31,9 @@ func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request,
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := &passwordlessPromptData{
userData: l.getUserData(r, authReq, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage),
}
translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessPromptData{
userData: l.getUserData(r, authReq, translator, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage),
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil)
}

View File

@@ -99,11 +99,10 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
if webAuthNToken != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData)
}
translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessRegistrationData{
webAuthNData{
userData: l.getUserData(r, authReq, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage),
CredentialCreationData: credentialData,
},
code,
@@ -117,8 +116,6 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false)
logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy")
data.LabelPolicy = labelPolicyToDomain(policy)
translator, err = l.renderer.NewTranslator(r.Context())
if err == nil {
texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts")
@@ -193,9 +190,8 @@ func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *htt
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := passwordlessRegistrationDoneDate{
userData: l.getUserData(r, authReq, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage),
HideNextButton: authReq == nil,
}
if authReq == nil {

View File

@@ -96,7 +96,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
l.renderRegister(w, r, authRequest, data, err)
return
}
user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
@@ -160,7 +159,7 @@ func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authReque
}
data := registerData{
baseData: l.getBaseData(r, authRequest, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage),
baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage),
registerFormData: *formData,
}

View File

@@ -54,7 +54,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut
}
translator := l.getTranslator(r.Context(), authReq)
data := registerOptionData{
baseData: l.getBaseData(r, authReq, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage),
}
funcs := map[string]interface{}{
"hasRegistration": func() bool {

View File

@@ -1,7 +1,6 @@
package login
import (
"context"
"net/http"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -39,8 +38,12 @@ type registerOrgData struct {
}
func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
if disallowed || err != nil {
restrictions, err := l.query.GetInstanceRestrictions(r.Context())
if err != nil {
l.renderError(w, r, nil, err)
return
}
if restrictions.DisallowPublicOrgRegistration {
w.WriteHeader(http.StatusNotFound)
return
}
@@ -54,8 +57,12 @@ func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
if disallowed || err != nil {
restrictions, err := l.query.GetInstanceRestrictions(r.Context())
if err != nil {
l.renderError(w, r, nil, err)
return
}
if restrictions.DisallowPublicOrgRegistration {
w.WriteHeader(http.StatusConflict)
return
}
@@ -99,7 +106,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
}
translator := l.getTranslator(r.Context(), authRequest)
data := registerOrgData{
baseData: l.getBaseData(r, authRequest, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage),
baseData: l.getBaseData(r, authRequest, translator, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage),
registerOrgFormData: *formData,
}
pwPolicy := l.getPasswordComplexityPolicy(r, "0")
@@ -130,11 +137,6 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil)
}
func (l *Login) publicOrgRegistrationIsDisallowed(ctx context.Context) (bool, error) {
restrictions, err := l.query.GetInstanceRestrictions(ctx)
return restrictions.DisallowPublicOrgRegistration, err
}
func (d registerOrgFormData) toUserDomain() *domain.Human {
if d.Username == "" {
d.Username = string(d.Email)

View File

@@ -39,7 +39,7 @@ type LanguageData struct {
Lang string
}
func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage static.Storage, cookieName string) *Renderer {
func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName string) *Renderer {
r := &Renderer{
pathPrefix: pathPrefix,
staticStorage: staticStorage,
@@ -238,7 +238,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
}
var err error
r.Renderer, err = renderer.NewRenderer(
staticDir,
tmplMapping, funcs,
cookieName,
)
@@ -343,13 +342,14 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth
_, msg = l.getErrorMessage(r, err)
}
data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg)
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplError], data, nil)
translator := l.getTranslator(r.Context(), authReq)
data := l.getBaseData(r, authReq, translator, "Errors.Internal", "", "Internal", msg)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil)
}
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData {
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData {
userData := userData{
baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, errType, errMessage),
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, errType, errMessage),
profileData: l.getProfileData(authReq),
}
if authReq != nil && authReq.LinkingUsers != nil {
@@ -358,9 +358,7 @@ func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI
return userData
}
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
translator := l.getTranslator(r.Context(), authReq)
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
title := ""
if titleI18nKey != "" {
title = translator.LocalizeWithoutArgs(titleI18nKey)
@@ -418,7 +416,11 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI
}
func (l *Login) getTranslator(ctx context.Context, authReq *domain.AuthRequest) *i18n.Translator {
translator, err := l.renderer.NewTranslator(ctx)
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, err := l.renderer.NewTranslator(ctx, restrictions.AllowedLanguages)
logging.OnError(err).Warn("cannot load translator")
if authReq != nil {
l.addLoginTranslations(translator, authReq.DefaultTranslations)

View File

@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/assets"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/i18n"
)
type dynamicResourceData struct {
@@ -15,8 +16,8 @@ type dynamicResourceData struct {
FileName string `schema:"filename"`
}
func (l *Login) handleResources(staticDir http.FileSystem) http.Handler {
return http.FileServer(staticDir)
func (l *Login) handleResources() http.Handler {
return http.FileServer(i18n.LoadFilesystem(i18n.LOGIN))
}
func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {

View File

@@ -64,7 +64,7 @@ var (
}
)
func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.MiddlewareFunc) *mux.Router {
func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router {
router := mux.NewRouter()
router.Use(interceptors...)
router.HandleFunc(EndpointRoot, login.handleLogin).Methods(http.MethodGet)
@@ -113,7 +113,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet)
router.HandleFunc(EndpointDynamicResources, login.handleDynamicResources).Methods(http.MethodGet)
router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet)
router.PathPrefix(EndpointResources).Handler(login.handleResources()).Methods(http.MethodGet)
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet)
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet)

View File

@@ -28,7 +28,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth
descriptionI18nKey = "SelectAccount.DescriptionLinking"
}
data := userSelectionData{
baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, "", ""),
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, "", ""),
Users: selectionData.Users,
Linking: linking,
}

View File

@@ -21,7 +21,7 @@ func (l *Login) renderChangeUsername(w http.ResponseWriter, r *http.Request, aut
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, "UsernameChange.Title", "UsernameChange.Description", errID, errMessage)
data := l.getUserData(r, authReq, translator, "UsernameChange.Title", "UsernameChange.Description", errID, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsername], data, nil)
}
@@ -43,6 +43,6 @@ func (l *Login) handleChangeUsername(w http.ResponseWriter, r *http.Request) {
func (l *Login) renderChangeUsernameDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
var errType, errMessage string
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, "UsernameChangeDone.Title", "UsernameChangeDone.Description", errType, errMessage)
data := l.getUserData(r, authReq, translator, "UsernameChangeDone.Title", "UsernameChangeDone.Description", errType, errMessage)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsernameDone], data, nil)
}