mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 00:47:33 +00:00
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:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
19
internal/api/grpc/admin/language_converter.go
Normal file
19
internal/api/grpc/admin/language_converter.go
Normal 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...)
|
||||
}
|
@@ -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,
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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",
|
||||
)
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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{}) {}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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{
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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...)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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, "")
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
@@ -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{
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user