mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 23:57:23 +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:
parent
236930f109
commit
dd33538c0a
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ sandbox.go
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
.run
|
||||||
|
|
||||||
# credential
|
# credential
|
||||||
google-credentials
|
google-credentials
|
||||||
|
2
Makefile
2
Makefile
@ -103,7 +103,7 @@ core_unit_test:
|
|||||||
core_integration_setup:
|
core_integration_setup:
|
||||||
go build -o zitadel main.go
|
go build -o zitadel main.go
|
||||||
./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
||||||
./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/zitadel.yaml --steps internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
||||||
$(RM) zitadel
|
$(RM) zitadel
|
||||||
|
|
||||||
.PHONY: core_integration_test
|
.PHONY: core_integration_test
|
||||||
|
@ -838,6 +838,11 @@ DefaultInstance:
|
|||||||
# DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org
|
# DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org
|
||||||
# If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests.
|
# If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests.
|
||||||
DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION
|
DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION
|
||||||
|
# AllowedLanguages restricts the languages that can be used.
|
||||||
|
# If the list is empty, all supported languages are allowed.
|
||||||
|
AllowedLanguages: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_ALLOWEDLANGUAGES
|
||||||
|
# - en
|
||||||
|
# - de
|
||||||
Quotas:
|
Quotas:
|
||||||
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
|
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
|
||||||
# The following unit types are supported
|
# The following unit types are supported
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
||||||
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
|
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/migration"
|
"github.com/zitadel/zitadel/internal/migration"
|
||||||
"github.com/zitadel/zitadel/internal/query/projection"
|
"github.com/zitadel/zitadel/internal/query/projection"
|
||||||
)
|
)
|
||||||
@ -64,6 +65,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
logging.Info("setup started")
|
logging.Info("setup started")
|
||||||
|
|
||||||
|
i18n.MustLoadSupportedLanguagesFromDir()
|
||||||
|
|
||||||
zitadelDBClient, err := database.Connect(config.Database, false, false)
|
zitadelDBClient, err := database.Connect(config.Database, false, false)
|
||||||
logging.OnError(err).Fatal("unable to connect to database")
|
logging.OnError(err).Fatal("unable to connect to database")
|
||||||
esPusherDBClient, err := database.Connect(config.Database, false, true)
|
esPusherDBClient, err := database.Connect(config.Database, false, true)
|
||||||
|
@ -62,6 +62,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
||||||
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
|
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/id"
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
"github.com/zitadel/zitadel/internal/logstore"
|
"github.com/zitadel/zitadel/internal/logstore"
|
||||||
"github.com/zitadel/zitadel/internal/logstore/emitters/access"
|
"github.com/zitadel/zitadel/internal/logstore/emitters/access"
|
||||||
@ -93,7 +94,6 @@ Requirements:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return startZitadel(config, masterKey, server)
|
return startZitadel(config, masterKey, server)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -123,6 +123,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
i18n.MustLoadSupportedLanguagesFromDir()
|
||||||
|
|
||||||
zitadelDBClient, err := database.Connect(config.Database, false, false)
|
zitadelDBClient, err := database.Connect(config.Database, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot start client for projection: %w", err)
|
return fmt.Errorf("cannot start client for projection: %w", err)
|
||||||
|
@ -8,7 +8,11 @@ Users with the role IAM_OWNER can change the restrictions of their instance usin
|
|||||||
Currently, the following restrictions are available:
|
Currently, the following restrictions are available:
|
||||||
|
|
||||||
- *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests.
|
- *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests.
|
||||||
- *[Coming soon](https://github.com/zitadel/zitadel/issues/6250): AllowedLanguages*
|
- *AllowedLanguages* - The following rules apply if languages are restricted:
|
||||||
|
- Only allowed languages are listed in the OIDC discovery endpoint */.well-kown/openid-configuration*.
|
||||||
|
- Login UI texts are only rendered in allowed languages.
|
||||||
|
- Notification message texts are only rendered in allowed languages.
|
||||||
|
- Custom Texts can be created for disallowed languages as long as ZITADEL supports that language. Therefore, all texts can be customized before allowing a language.
|
||||||
|
|
||||||
Feature restrictions for an instance are intended to be configured by a user that is managed within that instance.
|
Feature restrictions for an instance are intended to be configured by a user that is managed within that instance.
|
||||||
However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users.
|
However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users.
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,29 +3,23 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/text"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errors "github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
|
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) {
|
func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) {
|
||||||
langs, err := s.query.Languages(ctx)
|
return &admin_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine
|
human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine
|
||||||
|
|
||||||
createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
|
createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
|
||||||
Name: req.Org.Name,
|
Name: req.Org.Name,
|
||||||
CustomDomain: req.Org.Domain,
|
CustomDomain: req.Org.Domain,
|
||||||
|
@ -5,11 +5,19 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -26,5 +34,6 @@ func (s *Server) GetRestrictions(ctx context.Context, _ *admin.GetRestrictionsRe
|
|||||||
return &admin.GetRestrictionsResponse{
|
return &admin.GetRestrictionsResponse{
|
||||||
Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner),
|
Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner),
|
||||||
DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration,
|
DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration,
|
||||||
|
AllowedLanguages: domain.LanguagesToStrings(restrictions.AllowedLanguages),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
@ -29,19 +30,25 @@ func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
|
|||||||
jar, err := cookiejar.New(nil)
|
jar, err := cookiejar.New(nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
browserSession := &http.Client{Jar: jar}
|
browserSession := &http.Client{Jar: jar}
|
||||||
// Default should be allowed
|
var csrfToken string
|
||||||
csrfToken := awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
|
t.Run("public org registration is allowed by default", func(*testing.T) {
|
||||||
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
|
csrfToken = awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
|
||||||
require.NoError(t, err)
|
})
|
||||||
awaitDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken)
|
t.Run("disallowing public org registration disables the endpoints", func(*testing.T) {
|
||||||
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
|
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
|
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
|
// awaitPubOrgRegAllowed 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 {
|
func awaitPubOrgRegAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string {
|
||||||
csrfToken := awaitGetResponse(t, ctx, client, parsedURL, http.StatusOK)
|
csrfToken := awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusOK)
|
||||||
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken)
|
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken)
|
||||||
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
|
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -49,17 +56,17 @@ func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsed
|
|||||||
return csrfToken
|
return csrfToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// awaitDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
|
// awaitPubOrgRegDisallowed 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) {
|
func awaitPubOrgRegDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
|
||||||
awaitGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
|
awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
|
||||||
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken)
|
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken)
|
||||||
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
|
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, restrictions.DisallowPublicOrgRegistration)
|
require.True(t, restrictions.DisallowPublicOrgRegistration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// awaitGetResponse cuts the CSRF token from the response body if it exists
|
// awaitGetSSRGetResponse 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 {
|
func awaitGetSSRGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
|
||||||
var csrfToken []byte
|
var csrfToken []byte
|
||||||
await(t, ctx, func() bool {
|
await(t, ctx, func() bool {
|
||||||
resp, err := client.Get(parsedURL.String())
|
resp, err := client.Get(parsedURL.String())
|
||||||
@ -71,7 +78,7 @@ func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, pa
|
|||||||
if hasCsrfToken {
|
if hasCsrfToken {
|
||||||
csrfToken, _, _ = bytes.Cut(after, []byte(`">`))
|
csrfToken, _, _ = bytes.Cut(after, []byte(`">`))
|
||||||
}
|
}
|
||||||
return resp.StatusCode == expectCode
|
return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode)
|
||||||
})
|
})
|
||||||
return string(csrfToken)
|
return string(csrfToken)
|
||||||
}
|
}
|
||||||
@ -83,24 +90,6 @@ func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Clien
|
|||||||
"gorilla.csrf.Token": {csrfToken},
|
"gorilla.csrf.Token": {csrfToken},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
AdminCTX, SystemCTX context.Context
|
AdminCTX, SystemCTX context.Context
|
||||||
Tester *integration.Tester
|
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) {
|
func TestMain(m *testing.M) {
|
||||||
@ -30,3 +35,29 @@ func TestMain(m *testing.M) {
|
|||||||
return m.Run()
|
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 (
|
import (
|
||||||
"context"
|
"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"
|
auth_pb "github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) GetSupportedLanguages(ctx context.Context, req *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
|
func (s *Server) GetSupportedLanguages(context.Context, *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
|
||||||
langs, err := s.query.Languages(ctx)
|
return &auth_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &auth_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
|
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,12 @@ package management
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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"
|
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) GetSupportedLanguages(ctx context.Context, req *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
|
func (s *Server) GetSupportedLanguages(context.Context, *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
|
||||||
langs, err := s.query.Languages(ctx)
|
return &mgmt_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &mgmt_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, 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) {
|
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
|
||||||
human := AddHumanUserRequestToAddHuman(req)
|
human := AddHumanUserRequestToAddHuman(req)
|
||||||
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true)
|
if err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &mgmt_pb.AddHumanUserResponse{
|
return &mgmt_pb.AddHumanUserResponse{
|
||||||
|
@ -55,14 +55,14 @@ func TestImport_and_Get(t *testing.T) {
|
|||||||
// create unique names.
|
// create unique names.
|
||||||
lastName := strconv.FormatInt(time.Now().Unix(), 10)
|
lastName := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
userName := strings.Join([]string{firstName, lastName}, "_")
|
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{
|
res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
Profile: &management.ImportHumanUserRequest_Profile{
|
Profile: &management.ImportHumanUserRequest_Profile{
|
||||||
FirstName: firstName,
|
FirstName: firstName,
|
||||||
LastName: lastName,
|
LastName: lastName,
|
||||||
PreferredLanguage: language.Afrikaans.String(),
|
PreferredLanguage: language.Japanese.String(),
|
||||||
Gender: user.Gender_GENDER_DIVERSE,
|
Gender: user.Gender_GENDER_DIVERSE,
|
||||||
},
|
},
|
||||||
Email: &management.ImportHumanUserRequest_Email{
|
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 {
|
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")
|
logging.OnError(err).Panic("unable to get translator")
|
||||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
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...)
|
return setInstance(ctx, req, info, handler, verifier, headerName, translator, explicitInstanceIdServices...)
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
_ "github.com/zitadel/zitadel/internal/statik"
|
_ "github.com/zitadel/zitadel/internal/statik"
|
||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"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) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
if loc, ok := resp.(localizers); ok && resp != nil {
|
if loc, ok := resp.(localizers); ok && resp != nil {
|
||||||
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
|
translator, translatorError := getTranslator(ctx)
|
||||||
if translatorError != nil {
|
if translatorError != nil {
|
||||||
logging.New().WithError(translatorError).Error("could not load translator")
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
translateFields(ctx, loc, translator)
|
translateFields(ctx, loc, translator)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
|
translator, translatorError := getTranslator(ctx)
|
||||||
if translatorError != nil {
|
if translatorError != nil {
|
||||||
logging.New().WithError(translatorError).Error("could not load translator")
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
err = translateError(ctx, err, translator)
|
err = translateError(ctx, err, translator)
|
||||||
@ -36,3 +35,11 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc.
|
|||||||
return resp, err
|
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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/rakyll/statik/fs"
|
|
||||||
"github.com/zitadel/logging"
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
|
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/i18n"
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
)
|
)
|
||||||
@ -39,14 +35,3 @@ func translateError(ctx context.Context, err error, translator *i18n.Translator)
|
|||||||
}
|
}
|
||||||
return err
|
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/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"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"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/settings/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) {
|
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)
|
instance := authz.GetInstance(ctx)
|
||||||
return &settings.GetGeneralSettingsResponse{
|
return &settings.GetGeneralSettingsResponse{
|
||||||
SupportedLanguages: text.LanguageTagsToStrings(langs),
|
SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()),
|
||||||
DefaultOrgId: instance.DefaultOrganisationID(),
|
DefaultOrgId: instance.DefaultOrganisationID(),
|
||||||
DefaultLanguage: instance.DefaultLanguage().String(),
|
DefaultLanguage: instance.DefaultLanguage().String(),
|
||||||
}, nil
|
}, 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
|
return nil, err
|
||||||
}
|
}
|
||||||
orgID := authz.GetCtxData(ctx).OrgID
|
orgID := authz.GetCtxData(ctx).OrgID
|
||||||
err = s.command.AddHuman(ctx, orgID, human, false)
|
if err = s.command.AddHuman(ctx, orgID, human, false); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &user.AddHumanUserResponse{
|
return &user.AddHumanUserResponse{
|
||||||
|
@ -677,7 +677,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
|||||||
parametersEqual: map[string]string{
|
parametersEqual: map[string]string{
|
||||||
"client_id": "clientID",
|
"client_id": "clientID",
|
||||||
"prompt": "select_account",
|
"prompt": "select_account",
|
||||||
"redirect_uri": "http://localhost:8080/idps/callback",
|
"redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback",
|
||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
"scope": "openid profile email",
|
"scope": "openid profile email",
|
||||||
},
|
},
|
||||||
@ -704,7 +704,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
|||||||
ChangeDate: timestamppb.Now(),
|
ChangeDate: timestamppb.Now(),
|
||||||
ResourceOwner: Tester.Organisation.ID,
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
},
|
},
|
||||||
url: "http://localhost:8000/sso",
|
url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
|
||||||
parametersExisting: []string{"RelayState", "SAMLRequest"},
|
parametersExisting: []string{"RelayState", "SAMLRequest"},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@ -728,7 +728,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
|||||||
ChangeDate: timestamppb.Now(),
|
ChangeDate: timestamppb.Now(),
|
||||||
ResourceOwner: Tester.Organisation.ID,
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
},
|
},
|
||||||
url: "http://localhost:8000/sso",
|
url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
|
||||||
parametersExisting: []string{"RelayState", "SAMLRequest"},
|
parametersExisting: []string{"RelayState", "SAMLRequest"},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rakyll/statik/fs"
|
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
@ -120,10 +119,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newZitadelTranslator() *i18n.Translator {
|
func newZitadelTranslator() *i18n.Translator {
|
||||||
dir, err := fs.NewWithNamespace("zitadel")
|
translator, err := i18n.NewZitadelTranslator(language.English)
|
||||||
logging.WithFields("namespace", "zitadel").OnError(err).Panic("unable to get namespace")
|
|
||||||
|
|
||||||
translator, err := i18n.NewTranslator(dir, language.English, "")
|
|
||||||
logging.OnError(err).Panic("unable to get translator")
|
logging.OnError(err).Panic("unable to get translator")
|
||||||
return translator
|
return translator
|
||||||
}
|
}
|
||||||
|
18
internal/api/http/middleware/middleware_test.go
Normal file
18
internal/api/http/middleware/middleware_test.go
Normal 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()
|
||||||
|
}
|
@ -6,11 +6,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rakyll/statik/fs"
|
|
||||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
"golang.org/x/exp/slog"
|
"golang.org/x/exp/slog"
|
||||||
"golang.org/x/text/language"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/assets"
|
"github.com/zitadel/zitadel/internal/api/assets"
|
||||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||||
@ -23,7 +21,6 @@ import (
|
|||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
"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/query"
|
||||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
"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) {
|
func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) {
|
||||||
supportedLanguages, err := getSupportedLanguages()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
opConfig := &op.Config{
|
opConfig := &op.Config{
|
||||||
DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
|
DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
|
||||||
CodeMethodS256: config.CodeMethodS256,
|
CodeMethodS256: config.CodeMethodS256,
|
||||||
@ -178,7 +171,6 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []
|
|||||||
AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT,
|
AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT,
|
||||||
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
|
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
|
||||||
RequestObjectSupported: config.RequestObjectSupported,
|
RequestObjectSupported: config.RequestObjectSupported,
|
||||||
SupportedUILocales: supportedLanguages,
|
|
||||||
DeviceAuthorization: config.DeviceAuth.toOPConfig(),
|
DeviceAuthorization: config.DeviceAuth.toOPConfig(),
|
||||||
}
|
}
|
||||||
if cryptoLength := len(cryptoKey); cryptoLength != 32 {
|
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 {
|
func (o *OPStorage) Health(ctx context.Context) error {
|
||||||
return o.repo.Health(ctx)
|
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)
|
|
||||||
}
|
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/auth/repository"
|
"github.com/zitadel/zitadel/internal/auth/repository"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"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) {
|
func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
restrictions, err := s.query.GetInstanceRestrictions(ctx)
|
||||||
return op.NewResponse(s.createDiscoveryConfig(ctx)), nil
|
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) {
|
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)
|
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)
|
issuer := op.IssuerFromContext(ctx)
|
||||||
return &oidc.DiscoveryConfiguration{
|
return &oidc.DiscoveryConfiguration{
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
@ -231,7 +239,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfi
|
|||||||
RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()),
|
RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()),
|
||||||
ClaimsSupported: op.SupportedClaims(s.Provider()),
|
ClaimsSupported: op.SupportedClaims(s.Provider()),
|
||||||
CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()),
|
CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()),
|
||||||
UILocalesSupported: s.Provider().SupportedUILocales(),
|
UILocalesSupported: supportedUILocales,
|
||||||
RequestParameterSupported: s.Provider().RequestObjectSupported(),
|
RequestParameterSupported: s.Provider().RequestObjectSupported(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
|||||||
signingKeyAlgorithm string
|
signingKeyAlgorithm string
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
supportedUILocales []language.Tag
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -36,7 +37,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
|||||||
AuthMethodPrivateKeyJWT: true,
|
AuthMethodPrivateKeyJWT: true,
|
||||||
GrantTypeRefreshToken: true,
|
GrantTypeRefreshToken: true,
|
||||||
RequestObjectSupported: true,
|
RequestObjectSupported: true,
|
||||||
SupportedUILocales: []language.Tag{language.English, language.German},
|
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
@ -56,7 +56,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
|||||||
signingKeyAlgorithm: "RS256",
|
signingKeyAlgorithm: "RS256",
|
||||||
},
|
},
|
||||||
args{
|
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{
|
&oidc.DiscoveryConfiguration{
|
||||||
Issuer: "https://issuer.com",
|
Issuer: "https://issuer.com",
|
||||||
@ -113,7 +114,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
|||||||
LegacyServer: tt.fields.LegacyServer,
|
LegacyServer: tt.fields.LegacyServer,
|
||||||
signingKeyAlgorithm: tt.fields.signingKeyAlgorithm,
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
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 {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errType, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := passwordData{
|
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),
|
profileData: l.getProfileData(authReq),
|
||||||
}
|
}
|
||||||
policy := l.getPasswordComplexityPolicy(r, authReq.UserOrgID)
|
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) {
|
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||||
var errType, errMessage string
|
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
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)
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil)
|
||||||
}
|
}
|
||||||
|
@ -28,13 +28,13 @@ func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request,
|
|||||||
logging.WithError(err).Error()
|
logging.WithError(err).Error()
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
|
|
||||||
translator := l.getTranslator(r.Context(), nil)
|
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)
|
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) {
|
func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) {
|
||||||
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := &struct {
|
data := &struct {
|
||||||
baseData
|
baseData
|
||||||
AuthRequestID string
|
AuthRequestID string
|
||||||
@ -42,14 +42,13 @@ func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, a
|
|||||||
ClientID string
|
ClientID string
|
||||||
Scopes []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,
|
AuthRequestID: authReq.ID,
|
||||||
Username: authReq.UserName,
|
Username: authReq.UserName,
|
||||||
ClientID: authReq.ApplicationID,
|
ClientID: authReq.ApplicationID,
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
}
|
}
|
||||||
|
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
|
||||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil)
|
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.
|
// 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) {
|
func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) {
|
||||||
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := &struct {
|
data := &struct {
|
||||||
baseData
|
baseData
|
||||||
Message string
|
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 {
|
switch action {
|
||||||
case deviceAuthAllowed:
|
case deviceAuthAllowed:
|
||||||
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)
|
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)
|
||||||
|
@ -549,7 +549,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := externalNotFoundOptionData{
|
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{
|
externalNotFoundOptionFormData: externalNotFoundOptionFormData{
|
||||||
externalRegisterFormData: externalRegisterFormData{
|
externalRegisterFormData: externalRegisterFormData{
|
||||||
Email: human.EmailAddress,
|
Email: human.EmailAddress,
|
||||||
|
@ -122,7 +122,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
|
|||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
|
|
||||||
data := initPasswordData{
|
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),
|
profileData: l.getProfileData(authReq),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Code: code,
|
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) {
|
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)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
|
data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
|
||||||
if authReq == nil {
|
if authReq == nil {
|
||||||
l.customTexts(r.Context(), translator, orgID)
|
l.customTexts(r.Context(), translator, orgID)
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
|
|||||||
|
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := initUserData{
|
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),
|
profileData: l.getProfileData(authReq),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Code: code,
|
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) {
|
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)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
|
data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", "", "")
|
||||||
if authReq == nil {
|
if authReq == nil {
|
||||||
l.customTexts(r.Context(), translator, orgID)
|
l.customTexts(r.Context(), translator, orgID)
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,9 @@ func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq
|
|||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
temp := l.renderer.Templates[tmplLDAPLogin]
|
temp := l.renderer.Templates[tmplLDAPLogin]
|
||||||
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil)
|
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) {
|
func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -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) {
|
func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
|
||||||
var errType, errMessage string
|
var errType, errMessage string
|
||||||
data := l.getUserData(r, authReq, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLinkUsersDone], data, nil)
|
data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
|
||||||
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil)
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,12 @@ package login
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/rakyll/statik/fs"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/feature"
|
"github.com/zitadel/zitadel/feature"
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||||
@ -93,17 +90,12 @@ func CreateLogin(config Config,
|
|||||||
userCodeAlg: userCodeAlg,
|
userCodeAlg: userCodeAlg,
|
||||||
featureCheck: featureCheck,
|
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())
|
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
|
||||||
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
||||||
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
|
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
|
||||||
|
|
||||||
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
|
login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
|
||||||
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName)
|
login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName)
|
||||||
login.parser = form.NewParser()
|
login.parser = form.NewParser()
|
||||||
return login, nil
|
return login, nil
|
||||||
}
|
}
|
||||||
|
@ -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)
|
l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID)
|
||||||
return
|
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{}{
|
funcs := map[string]interface{}{
|
||||||
"hasUsernamePasswordLogin": func() bool {
|
"hasUsernamePasswordLogin": func() bool {
|
||||||
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword
|
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
|
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 {
|
func singleIDPAllowed(authReq *domain.AuthRequest) bool {
|
||||||
|
@ -41,8 +41,9 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := loginSuccessData{
|
data := loginSuccessData{
|
||||||
userData: l.getUserData(r, authReq, "LoginSuccess.Title", "", errID, errMessage),
|
userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", errID, errMessage),
|
||||||
}
|
}
|
||||||
if authReq != nil {
|
if authReq != nil {
|
||||||
data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq)
|
data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq)
|
||||||
@ -51,7 +52,7 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
|
|||||||
return
|
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) {
|
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||||
|
@ -13,6 +13,7 @@ func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) renderLogoutDone(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", "", "")
|
translator := l.getTranslator(r.Context(), nil)
|
||||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), nil), l.renderer.Templates[tmplLogoutDone], data, nil)
|
data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", "", "")
|
||||||
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogoutDone], data, nil)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
|
|||||||
|
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := mailVerificationData{
|
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,
|
UserID: userID,
|
||||||
profileData: l.getProfileData(authReq),
|
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) {
|
func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := mailVerificationData{
|
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),
|
profileData: l.getProfileData(authReq),
|
||||||
}
|
}
|
||||||
if authReq == nil {
|
if authReq == nil {
|
||||||
|
@ -16,7 +16,7 @@ type mfaInitDoneData struct {
|
|||||||
func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) {
|
func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) {
|
||||||
var errType, errMessage string
|
var errType, errMessage string
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
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)
|
data.profileData = l.getProfileData(authReq)
|
||||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil)
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil)
|
||||||
}
|
}
|
||||||
|
@ -57,10 +57,11 @@ func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authRe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
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.profileData = l.getProfileData(authReq)
|
||||||
data.MFAType = domain.MFATypeOTPSMS
|
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.
|
// handleRegisterSMSCheck handles form submissions of the SMS registration.
|
||||||
|
@ -29,14 +29,15 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe
|
|||||||
if u2f != nil {
|
if u2f != nil {
|
||||||
credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData)
|
credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData)
|
||||||
}
|
}
|
||||||
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := &u2fInitData{
|
data := &u2fInitData{
|
||||||
webAuthNData: webAuthNData{
|
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,
|
CredentialCreationData: credentialData,
|
||||||
},
|
},
|
||||||
MFAType: domain.MFATypeU2F,
|
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) {
|
func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -71,7 +71,7 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth
|
|||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
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)
|
data.profileData = l.getProfileData(authReq)
|
||||||
if data.MFAType == domain.MFATypeTOTP {
|
if data.MFAType == domain.MFATypeTOTP {
|
||||||
code, err := generateQrCode(data.totpData.Url)
|
code, err := generateQrCode(data.totpData.Url)
|
||||||
|
@ -56,7 +56,7 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq
|
|||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := mfaData{
|
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),
|
profileData: l.getProfileData(authReq),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,12 +66,12 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
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 {
|
if verificationStep == nil {
|
||||||
l.renderError(w, r, authReq, err)
|
l.renderError(w, r, authReq, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
|
||||||
|
|
||||||
switch selectedProvider {
|
switch selectedProvider {
|
||||||
case domain.MFATypeU2F:
|
case domain.MFATypeU2F:
|
||||||
|
@ -61,12 +61,13 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := &mfaOTPData{
|
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),
|
MFAProviders: removeSelectedProviderFromList(providers, selectedProvider),
|
||||||
SelectedProvider: 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.
|
// handleOTPVerificationCheck handles form submissions of the OTP verification.
|
||||||
|
@ -37,15 +37,16 @@ func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, au
|
|||||||
if webAuthNLogin != nil {
|
if webAuthNLogin != nil {
|
||||||
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
|
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
|
||||||
}
|
}
|
||||||
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := &mfaU2FData{
|
data := &mfaU2FData{
|
||||||
webAuthNData: webAuthNData{
|
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,
|
CredentialCreationData: credentialData,
|
||||||
},
|
},
|
||||||
MFAProviders: providers,
|
MFAProviders: providers,
|
||||||
SelectedProvider: -1,
|
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) {
|
func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -19,7 +19,8 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
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{}{
|
funcs := map[string]interface{}{
|
||||||
"showPasswordReset": func() bool {
|
"showPasswordReset": func() bool {
|
||||||
if authReq.LoginPolicy != nil {
|
if authReq.LoginPolicy != nil {
|
||||||
@ -28,7 +29,7 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
|
|||||||
return true
|
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) {
|
func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -48,6 +48,7 @@ func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
data := l.getUserData(r, authReq, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordResetDone], data, nil)
|
data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
|
||||||
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordResetDone], data, nil)
|
||||||
}
|
}
|
||||||
|
@ -36,14 +36,15 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re
|
|||||||
if passwordSet && authReq.LoginPolicy != nil {
|
if passwordSet && authReq.LoginPolicy != nil {
|
||||||
passwordSet = authReq.LoginPolicy.AllowUsernamePassword
|
passwordSet = authReq.LoginPolicy.AllowUsernamePassword
|
||||||
}
|
}
|
||||||
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := &passwordlessData{
|
data := &passwordlessData{
|
||||||
webAuthNData{
|
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,
|
CredentialCreationData: credentialData,
|
||||||
},
|
},
|
||||||
passwordSet,
|
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) {
|
func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -31,10 +31,9 @@ func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errID, errMessage = l.getErrorMessage(r, err)
|
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)
|
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)
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil)
|
||||||
}
|
}
|
||||||
|
@ -99,11 +99,10 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
|
|||||||
if webAuthNToken != nil {
|
if webAuthNToken != nil {
|
||||||
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData)
|
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := &passwordlessRegistrationData{
|
data := &passwordlessRegistrationData{
|
||||||
webAuthNData{
|
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,
|
CredentialCreationData: credentialData,
|
||||||
},
|
},
|
||||||
code,
|
code,
|
||||||
@ -117,8 +116,6 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
|
|||||||
policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false)
|
policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false)
|
||||||
logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy")
|
logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy")
|
||||||
data.LabelPolicy = labelPolicyToDomain(policy)
|
data.LabelPolicy = labelPolicyToDomain(policy)
|
||||||
|
|
||||||
translator, err = l.renderer.NewTranslator(r.Context())
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
|
texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
|
||||||
logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts")
|
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)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
|
|
||||||
data := passwordlessRegistrationDoneDate{
|
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,
|
HideNextButton: authReq == nil,
|
||||||
}
|
}
|
||||||
if authReq == nil {
|
if authReq == nil {
|
||||||
|
@ -96,7 +96,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
l.renderRegister(w, r, authRequest, data, err)
|
l.renderRegister(w, r, authRequest, data, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
|
user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.renderRegister(w, r, authRequest, data, err)
|
l.renderRegister(w, r, authRequest, data, err)
|
||||||
@ -160,7 +159,7 @@ func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authReque
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := registerData{
|
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,
|
registerFormData: *formData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut
|
|||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
data := registerOptionData{
|
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{}{
|
funcs := map[string]interface{}{
|
||||||
"hasRegistration": func() bool {
|
"hasRegistration": func() bool {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package login
|
package login
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
@ -39,8 +38,12 @@ type registerOrgData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
|
func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
|
||||||
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
|
restrictions, err := l.query.GetInstanceRestrictions(r.Context())
|
||||||
if disallowed || err != nil {
|
if err != nil {
|
||||||
|
l.renderError(w, r, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if restrictions.DisallowPublicOrgRegistration {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
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) {
|
func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
|
restrictions, err := l.query.GetInstanceRestrictions(r.Context())
|
||||||
if disallowed || err != nil {
|
if err != nil {
|
||||||
|
l.renderError(w, r, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if restrictions.DisallowPublicOrgRegistration {
|
||||||
w.WriteHeader(http.StatusConflict)
|
w.WriteHeader(http.StatusConflict)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -99,7 +106,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
|
|||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authRequest)
|
translator := l.getTranslator(r.Context(), authRequest)
|
||||||
data := registerOrgData{
|
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,
|
registerOrgFormData: *formData,
|
||||||
}
|
}
|
||||||
pwPolicy := l.getPasswordComplexityPolicy(r, "0")
|
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)
|
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 {
|
func (d registerOrgFormData) toUserDomain() *domain.Human {
|
||||||
if d.Username == "" {
|
if d.Username == "" {
|
||||||
d.Username = string(d.Email)
|
d.Username = string(d.Email)
|
||||||
|
@ -39,7 +39,7 @@ type LanguageData struct {
|
|||||||
Lang string
|
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{
|
r := &Renderer{
|
||||||
pathPrefix: pathPrefix,
|
pathPrefix: pathPrefix,
|
||||||
staticStorage: staticStorage,
|
staticStorage: staticStorage,
|
||||||
@ -238,7 +238,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
r.Renderer, err = renderer.NewRenderer(
|
r.Renderer, err = renderer.NewRenderer(
|
||||||
staticDir,
|
|
||||||
tmplMapping, funcs,
|
tmplMapping, funcs,
|
||||||
cookieName,
|
cookieName,
|
||||||
)
|
)
|
||||||
@ -343,13 +342,14 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth
|
|||||||
|
|
||||||
_, msg = l.getErrorMessage(r, err)
|
_, msg = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg)
|
translator := l.getTranslator(r.Context(), authReq)
|
||||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplError], data, nil)
|
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{
|
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),
|
profileData: l.getProfileData(authReq),
|
||||||
}
|
}
|
||||||
if authReq != nil && authReq.LinkingUsers != nil {
|
if authReq != nil && authReq.LinkingUsers != nil {
|
||||||
@ -358,9 +358,7 @@ func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI
|
|||||||
return userData
|
return userData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
|
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
|
||||||
|
|
||||||
title := ""
|
title := ""
|
||||||
if titleI18nKey != "" {
|
if titleI18nKey != "" {
|
||||||
title = translator.LocalizeWithoutArgs(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 {
|
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")
|
logging.OnError(err).Warn("cannot load translator")
|
||||||
if authReq != nil {
|
if authReq != nil {
|
||||||
l.addLoginTranslations(translator, authReq.DefaultTranslations)
|
l.addLoginTranslations(translator, authReq.DefaultTranslations)
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/assets"
|
"github.com/zitadel/zitadel/internal/api/assets"
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dynamicResourceData struct {
|
type dynamicResourceData struct {
|
||||||
@ -15,8 +16,8 @@ type dynamicResourceData struct {
|
|||||||
FileName string `schema:"filename"`
|
FileName string `schema:"filename"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) handleResources(staticDir http.FileSystem) http.Handler {
|
func (l *Login) handleResources() http.Handler {
|
||||||
return http.FileServer(staticDir)
|
return http.FileServer(i18n.LoadFilesystem(i18n.LOGIN))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {
|
func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -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 := mux.NewRouter()
|
||||||
router.Use(interceptors...)
|
router.Use(interceptors...)
|
||||||
router.HandleFunc(EndpointRoot, login.handleLogin).Methods(http.MethodGet)
|
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(EndpointExternalRegisterCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
|
||||||
router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet)
|
router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet)
|
||||||
router.HandleFunc(EndpointDynamicResources, login.handleDynamicResources).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.handleRegisterOrg).Methods(http.MethodGet)
|
||||||
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost)
|
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost)
|
||||||
router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet)
|
router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet)
|
||||||
|
@ -28,7 +28,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth
|
|||||||
descriptionI18nKey = "SelectAccount.DescriptionLinking"
|
descriptionI18nKey = "SelectAccount.DescriptionLinking"
|
||||||
}
|
}
|
||||||
data := userSelectionData{
|
data := userSelectionData{
|
||||||
baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, "", ""),
|
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, "", ""),
|
||||||
Users: selectionData.Users,
|
Users: selectionData.Users,
|
||||||
Linking: linking,
|
Linking: linking,
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ func (l *Login) renderChangeUsername(w http.ResponseWriter, r *http.Request, aut
|
|||||||
errID, errMessage = l.getErrorMessage(r, err)
|
errID, errMessage = l.getErrorMessage(r, err)
|
||||||
}
|
}
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
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)
|
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) {
|
func (l *Login) renderChangeUsernameDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||||
var errType, errMessage string
|
var errType, errMessage string
|
||||||
translator := l.getTranslator(r.Context(), authReq)
|
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)
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsernameDone], data, nil)
|
||||||
}
|
}
|
||||||
|
22
internal/command/command_test.go
Normal file
22
internal/command/command_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
SupportedLanguages = []language.Tag{language.English, language.German}
|
||||||
|
OnlyAllowedLanguages = []language.Tag{language.English}
|
||||||
|
AllowedLanguage = language.English
|
||||||
|
DisallowedLanguage = language.German
|
||||||
|
UnsupportedLanguage = language.Spanish
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
i18n.SupportLanguages(SupportedLanguages...)
|
||||||
|
m.Run()
|
||||||
|
}
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/id"
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||||
@ -107,18 +108,22 @@ type InstanceSetup struct {
|
|||||||
EmailTemplate []byte
|
EmailTemplate []byte
|
||||||
MessageTexts []*domain.CustomMessageText
|
MessageTexts []*domain.CustomMessageText
|
||||||
SMTPConfiguration *smtp.Config
|
SMTPConfiguration *smtp.Config
|
||||||
OIDCSettings *struct {
|
OIDCSettings *OIDCSettings
|
||||||
AccessTokenLifetime time.Duration
|
Quotas *SetQuotas
|
||||||
IdTokenLifetime time.Duration
|
Features map[domain.Feature]any
|
||||||
RefreshTokenIdleExpiration time.Duration
|
Limits *SetLimits
|
||||||
RefreshTokenExpiration time.Duration
|
Restrictions *SetRestrictions
|
||||||
}
|
}
|
||||||
Quotas *struct {
|
|
||||||
Items []*SetQuota
|
type OIDCSettings struct {
|
||||||
}
|
AccessTokenLifetime time.Duration
|
||||||
Features map[domain.Feature]any
|
IdTokenLifetime time.Duration
|
||||||
Limits *SetLimits
|
RefreshTokenIdleExpiration time.Duration
|
||||||
Restrictions *SetRestrictions
|
RefreshTokenExpiration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetQuotas struct {
|
||||||
|
Items []*SetQuota
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecretGenerators struct {
|
type SecretGenerators struct {
|
||||||
@ -289,183 +294,32 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
|||||||
|
|
||||||
prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate),
|
prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate),
|
||||||
}
|
}
|
||||||
|
if err := setupQuotas(c, &validations, setup.Quotas, instanceID); err != nil {
|
||||||
if setup.Quotas != nil {
|
return "", "", nil, nil, err
|
||||||
for _, q := range setup.Quotas.Items {
|
|
||||||
quotaId, err := c.idGenerator.Next()
|
|
||||||
if err != nil {
|
|
||||||
return "", "", nil, nil, err
|
|
||||||
}
|
|
||||||
validations = append(validations, c.SetQuotaCommand(quota.NewAggregate(quotaId, instanceID), nil, true, q))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
|
||||||
for _, msg := range setup.MessageTexts {
|
|
||||||
validations = append(validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg))
|
|
||||||
}
|
|
||||||
|
|
||||||
console := &addOIDCApp{
|
|
||||||
AddApp: AddApp{
|
|
||||||
Aggregate: *projectAgg,
|
|
||||||
ID: setup.zitadel.consoleAppID,
|
|
||||||
Name: consoleAppName,
|
|
||||||
},
|
|
||||||
Version: domain.OIDCVersionV1,
|
|
||||||
RedirectUris: []string{},
|
|
||||||
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
|
|
||||||
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
|
|
||||||
ApplicationType: domain.OIDCApplicationTypeUserAgent,
|
|
||||||
AuthMethodType: domain.OIDCAuthMethodTypeNone,
|
|
||||||
PostLogoutRedirectUris: []string{},
|
|
||||||
DevMode: !c.externalSecure,
|
|
||||||
AccessTokenType: domain.OIDCTokenTypeBearer,
|
|
||||||
AccessTokenRoleAssertion: false,
|
|
||||||
IDTokenRoleAssertion: false,
|
|
||||||
IDTokenUserinfoAssertion: false,
|
|
||||||
ClockSkew: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
validations = append(validations,
|
validations = append(validations,
|
||||||
AddOrgCommand(ctx, orgAgg, setup.Org.Name),
|
AddOrgCommand(ctx, orgAgg, setup.Org.Name),
|
||||||
c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID),
|
c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID),
|
||||||
)
|
)
|
||||||
|
pat, machineKey, err := setupAdmin(c, &validations, setup.Org.Machine, setup.Org.Human, orgID, userID, userAgg)
|
||||||
var pat *PersonalAccessToken
|
|
||||||
var machineKey *MachineKey
|
|
||||||
// only a human or a machine user should be created as owner
|
|
||||||
if setup.Org.Machine != nil && setup.Org.Machine.Machine != nil && !setup.Org.Machine.Machine.IsZero() {
|
|
||||||
validations = append(validations,
|
|
||||||
AddMachineCommand(userAgg, setup.Org.Machine.Machine),
|
|
||||||
)
|
|
||||||
if setup.Org.Machine.Pat != nil {
|
|
||||||
pat = NewPersonalAccessToken(orgID, userID, setup.Org.Machine.Pat.ExpirationDate, setup.Org.Machine.Pat.Scopes, domain.UserTypeMachine)
|
|
||||||
pat.TokenID, err = c.idGenerator.Next()
|
|
||||||
if err != nil {
|
|
||||||
return "", "", nil, nil, err
|
|
||||||
}
|
|
||||||
validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm))
|
|
||||||
}
|
|
||||||
if setup.Org.Machine.MachineKey != nil {
|
|
||||||
machineKey = NewMachineKey(orgID, userID, setup.Org.Machine.MachineKey.ExpirationDate, setup.Org.Machine.MachineKey.Type)
|
|
||||||
machineKey.KeyID, err = c.idGenerator.Next()
|
|
||||||
if err != nil {
|
|
||||||
return "", "", nil, nil, err
|
|
||||||
}
|
|
||||||
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
|
|
||||||
}
|
|
||||||
} else if setup.Org.Human != nil {
|
|
||||||
setup.Org.Human.ID = userID
|
|
||||||
validations = append(validations,
|
|
||||||
c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordHasher, c.userEncryption, true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
validations = append(validations,
|
|
||||||
c.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner),
|
|
||||||
c.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner),
|
|
||||||
AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified),
|
|
||||||
SetIAMProject(instanceAgg, projectAgg.ID),
|
|
||||||
|
|
||||||
c.AddAPIAppCommand(
|
|
||||||
&addAPIApp{
|
|
||||||
AddApp: AddApp{
|
|
||||||
Aggregate: *projectAgg,
|
|
||||||
ID: setup.zitadel.mgmtAppID,
|
|
||||||
Name: mgmtAppName,
|
|
||||||
},
|
|
||||||
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
),
|
|
||||||
|
|
||||||
c.AddAPIAppCommand(
|
|
||||||
&addAPIApp{
|
|
||||||
AddApp: AddApp{
|
|
||||||
Aggregate: *projectAgg,
|
|
||||||
ID: setup.zitadel.adminAppID,
|
|
||||||
Name: adminAppName,
|
|
||||||
},
|
|
||||||
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
),
|
|
||||||
|
|
||||||
c.AddAPIAppCommand(
|
|
||||||
&addAPIApp{
|
|
||||||
AddApp: AddApp{
|
|
||||||
Aggregate: *projectAgg,
|
|
||||||
ID: setup.zitadel.authAppID,
|
|
||||||
Name: authAppName,
|
|
||||||
},
|
|
||||||
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
),
|
|
||||||
|
|
||||||
c.AddOIDCAppCommand(console, nil),
|
|
||||||
SetIAMConsoleID(instanceAgg, &console.ClientID, &setup.zitadel.consoleAppID),
|
|
||||||
)
|
|
||||||
|
|
||||||
addGeneratedDomain, err := c.addGeneratedInstanceDomain(ctx, instanceAgg, setup.InstanceName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return "", "", nil, nil, err
|
||||||
}
|
}
|
||||||
validations = append(validations, addGeneratedDomain...)
|
setupMinimalInterfaces(c, &validations, instanceAgg, projectAgg, orgAgg, userID, setup.zitadel)
|
||||||
if setup.CustomDomain != "" {
|
if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil {
|
||||||
validations = append(validations,
|
return "", "", nil, nil, err
|
||||||
c.addInstanceDomain(instanceAgg, setup.CustomDomain, false),
|
|
||||||
setPrimaryInstanceDomain(instanceAgg, setup.CustomDomain),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
|
||||||
if setup.SMTPConfiguration != nil {
|
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
|
||||||
validations = append(validations,
|
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
|
||||||
c.prepareAddSMTPConfig(
|
if err := setupFeatures(c, &validations, setup.Features, instanceID); err != nil {
|
||||||
instanceAgg,
|
return "", "", nil, nil, err
|
||||||
setup.SMTPConfiguration.From,
|
|
||||||
setup.SMTPConfiguration.FromName,
|
|
||||||
setup.SMTPConfiguration.ReplyToAddress,
|
|
||||||
setup.SMTPConfiguration.SMTP.Host,
|
|
||||||
setup.SMTPConfiguration.SMTP.User,
|
|
||||||
[]byte(setup.SMTPConfiguration.SMTP.Password),
|
|
||||||
setup.SMTPConfiguration.Tls,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if setup.OIDCSettings != nil {
|
|
||||||
validations = append(validations,
|
|
||||||
c.prepareAddOIDCSettings(
|
|
||||||
instanceAgg,
|
|
||||||
setup.OIDCSettings.AccessTokenLifetime,
|
|
||||||
setup.OIDCSettings.IdTokenLifetime,
|
|
||||||
setup.OIDCSettings.RefreshTokenIdleExpiration,
|
|
||||||
setup.OIDCSettings.RefreshTokenExpiration,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for f, value := range setup.Features {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case bool:
|
|
||||||
wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", nil, nil, err
|
|
||||||
}
|
|
||||||
validations = append(validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, c.idGenerator))
|
|
||||||
default:
|
|
||||||
return "", "", nil, nil, errors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if setup.Limits != nil {
|
|
||||||
validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setup.Limits))
|
|
||||||
}
|
|
||||||
|
|
||||||
if setup.Restrictions != nil {
|
|
||||||
validations = append(validations, c.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setup.Restrictions))
|
|
||||||
}
|
}
|
||||||
|
setupLimits(c, &validations, limitsAgg, setup.Limits)
|
||||||
|
setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions)
|
||||||
|
|
||||||
|
//nolint:staticcheck
|
||||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return "", "", nil, nil, err
|
||||||
@ -488,6 +342,205 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupLimits(commands *Commands, validations *[]preparation.Validation, limitsAgg *limits.Aggregate, setLimits *SetLimits) {
|
||||||
|
if setLimits != nil {
|
||||||
|
*validations = append(*validations, commands.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setLimits))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRestrictions(commands *Commands, validations *[]preparation.Validation, restrictionsAgg *restrictions.Aggregate, setRestrictions *SetRestrictions) {
|
||||||
|
if setRestrictions != nil {
|
||||||
|
*validations = append(*validations, commands.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setRestrictions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQuotas *SetQuotas, instanceID string) error {
|
||||||
|
if setQuotas == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, q := range setQuotas.Items {
|
||||||
|
quotaId, err := commands.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*validations = append(*validations, commands.SetQuotaCommand(quota.NewAggregate(quotaId, instanceID), nil, true, q))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupFeatures(commands *Commands, validations *[]preparation.Validation, enableFeatures map[domain.Feature]any, instanceID string) error {
|
||||||
|
for f, value := range enableFeatures {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case bool:
|
||||||
|
wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*validations = append(*validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, commands.idGenerator))
|
||||||
|
default:
|
||||||
|
return errors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) {
|
||||||
|
if oidcSettings == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*validations = append(*validations,
|
||||||
|
commands.prepareAddOIDCSettings(
|
||||||
|
instanceAgg,
|
||||||
|
oidcSettings.AccessTokenLifetime,
|
||||||
|
oidcSettings.IdTokenLifetime,
|
||||||
|
oidcSettings.RefreshTokenIdleExpiration,
|
||||||
|
oidcSettings.RefreshTokenExpiration,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupSMTPSettings(commands *Commands, validations *[]preparation.Validation, smtpConfig *smtp.Config, instanceAgg *instance.Aggregate) {
|
||||||
|
if smtpConfig == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*validations = append(*validations,
|
||||||
|
commands.prepareAddSMTPConfig(
|
||||||
|
instanceAgg,
|
||||||
|
smtpConfig.From,
|
||||||
|
smtpConfig.FromName,
|
||||||
|
smtpConfig.ReplyToAddress,
|
||||||
|
smtpConfig.SMTP.Host,
|
||||||
|
smtpConfig.SMTP.User,
|
||||||
|
[]byte(smtpConfig.SMTP.Password),
|
||||||
|
smtpConfig.Tls,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupCustomDomain(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, customDomain string) {
|
||||||
|
if customDomain == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*validations = append(*validations,
|
||||||
|
commands.addInstanceDomain(instanceAgg, customDomain, false),
|
||||||
|
setPrimaryInstanceDomain(instanceAgg, customDomain),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupGeneratedDomain(ctx context.Context, commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, instanceName string) error {
|
||||||
|
addGeneratedDomain, err := commands.addGeneratedInstanceDomain(ctx, instanceAgg, instanceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*validations = append(*validations, addGeneratedDomain...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, projectAgg *project.Aggregate, orgAgg *org.Aggregate, userID string, ids ZitadelConfig) {
|
||||||
|
cnsl := &addOIDCApp{
|
||||||
|
AddApp: AddApp{
|
||||||
|
Aggregate: *projectAgg,
|
||||||
|
ID: ids.consoleAppID,
|
||||||
|
Name: consoleAppName,
|
||||||
|
},
|
||||||
|
Version: domain.OIDCVersionV1,
|
||||||
|
RedirectUris: []string{},
|
||||||
|
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
|
||||||
|
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
|
||||||
|
ApplicationType: domain.OIDCApplicationTypeUserAgent,
|
||||||
|
AuthMethodType: domain.OIDCAuthMethodTypeNone,
|
||||||
|
PostLogoutRedirectUris: []string{},
|
||||||
|
DevMode: !commands.externalSecure,
|
||||||
|
AccessTokenType: domain.OIDCTokenTypeBearer,
|
||||||
|
AccessTokenRoleAssertion: false,
|
||||||
|
IDTokenRoleAssertion: false,
|
||||||
|
IDTokenUserinfoAssertion: false,
|
||||||
|
ClockSkew: 0,
|
||||||
|
}
|
||||||
|
*validations = append(*validations,
|
||||||
|
commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner),
|
||||||
|
commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner),
|
||||||
|
AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified),
|
||||||
|
SetIAMProject(instanceAgg, projectAgg.ID),
|
||||||
|
|
||||||
|
commands.AddAPIAppCommand(
|
||||||
|
&addAPIApp{
|
||||||
|
AddApp: AddApp{
|
||||||
|
Aggregate: *projectAgg,
|
||||||
|
ID: ids.mgmtAppID,
|
||||||
|
Name: mgmtAppName,
|
||||||
|
},
|
||||||
|
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
|
||||||
|
commands.AddAPIAppCommand(
|
||||||
|
&addAPIApp{
|
||||||
|
AddApp: AddApp{
|
||||||
|
Aggregate: *projectAgg,
|
||||||
|
ID: ids.adminAppID,
|
||||||
|
Name: adminAppName,
|
||||||
|
},
|
||||||
|
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
|
||||||
|
commands.AddAPIAppCommand(
|
||||||
|
&addAPIApp{
|
||||||
|
AddApp: AddApp{
|
||||||
|
Aggregate: *projectAgg,
|
||||||
|
ID: ids.authAppID,
|
||||||
|
Name: authAppName,
|
||||||
|
},
|
||||||
|
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
|
||||||
|
commands.AddOIDCAppCommand(cnsl, nil),
|
||||||
|
SetIAMConsoleID(instanceAgg, &cnsl.ClientID, &ids.consoleAppID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, human *AddHuman, orgID, userID string, userAgg *user.Aggregate) (pat *PersonalAccessToken, machineKey *MachineKey, err error) {
|
||||||
|
// only a human or a machine user should be created as owner
|
||||||
|
if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() {
|
||||||
|
*validations = append(*validations,
|
||||||
|
AddMachineCommand(userAgg, machine.Machine),
|
||||||
|
)
|
||||||
|
if machine.Pat != nil {
|
||||||
|
pat = NewPersonalAccessToken(orgID, userID, machine.Pat.ExpirationDate, machine.Pat.Scopes, domain.UserTypeMachine)
|
||||||
|
pat.TokenID, err = commands.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
*validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm))
|
||||||
|
}
|
||||||
|
if machine.MachineKey != nil {
|
||||||
|
machineKey = NewMachineKey(orgID, userID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type)
|
||||||
|
machineKey.KeyID, err = commands.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
*validations = append(*validations, prepareAddUserMachineKey(machineKey, commands.machineKeySize))
|
||||||
|
}
|
||||||
|
} else if human != nil {
|
||||||
|
human.ID = userID
|
||||||
|
*validations = append(*validations,
|
||||||
|
commands.AddHumanCommand(human, orgID, commands.userPasswordHasher, commands.userEncryption, true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return pat, machineKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts []*domain.CustomMessageText, instanceAgg *instance.Aggregate) {
|
||||||
|
for _, msg := range setupMessageTexts {
|
||||||
|
*validations = append(*validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) {
|
func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) {
|
||||||
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
|
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
|
||||||
validation := c.prepareUpdateInstance(instanceAgg, name)
|
validation := c.prepareUpdateInstance(instanceAgg, name)
|
||||||
@ -656,16 +709,27 @@ func (c *Commands) prepareUpdateInstance(a *instance.Aggregate, name string) pre
|
|||||||
|
|
||||||
func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation {
|
func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation {
|
||||||
return func() (preparation.CreateCommands, error) {
|
return func() (preparation.CreateCommands, error) {
|
||||||
if defaultLanguage == language.Und {
|
if err := domain.LanguageIsDefined(defaultLanguage); err != nil {
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument")
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := domain.LanguagesAreSupported(i18n.SupportedLanguages(), defaultLanguage); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||||
writeModel, err := getInstanceWriteModel(ctx, filter)
|
writeModel, err := getInstanceWriteModel(ctx, filter)
|
||||||
|
if writeModel.DefaultLanguage == defaultLanguage {
|
||||||
|
return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged")
|
||||||
|
}
|
||||||
|
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||||
|
restrictionsWM, err := c.getRestrictionsWriteModel(ctx, instanceID, instanceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if writeModel.DefaultLanguage == defaultLanguage {
|
if err := domain.LanguageIsAllowed(false, restrictionsWM.allowedLanguages, defaultLanguage); err != nil {
|
||||||
return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged")
|
return nil, err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return []eventstore.Command{instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage)}, nil
|
return []eventstore.Command{instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage)}, nil
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -2,16 +2,18 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetCustomInstanceLoginText only validates if the language is supported, not if it is allowed.
|
||||||
|
// This enables setting texts before allowing a language
|
||||||
func (c *Commands) SetCustomInstanceLoginText(ctx context.Context, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) {
|
func (c *Commands) SetCustomInstanceLoginText(ctx context.Context, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) {
|
||||||
iamAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
|
iamAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
|
||||||
events, existingMailText, err := c.setCustomInstanceLoginText(ctx, &iamAgg.Aggregate, loginText)
|
events, existingMailText, err := c.setCustomInstanceLoginText(ctx, &iamAgg.Aggregate, loginText)
|
||||||
@ -53,8 +55,8 @@ func (c *Commands) RemoveCustomInstanceLoginTexts(ctx context.Context, lang lang
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) setCustomInstanceLoginText(ctx context.Context, instanceAgg *eventstore.Aggregate, text *domain.CustomLoginText) ([]eventstore.Command, *InstanceCustomLoginTextReadModel, error) {
|
func (c *Commands) setCustomInstanceLoginText(ctx context.Context, instanceAgg *eventstore.Aggregate, text *domain.CustomLoginText) ([]eventstore.Command, *InstanceCustomLoginTextReadModel, error) {
|
||||||
if !text.IsValid() {
|
if err := text.IsValid(i18n.SupportedLanguages()); err != nil {
|
||||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "Instance-kd9fs", "Errors.CustomText.Invalid")
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
existingLoginText, err := c.defaultLoginTextWriteModelByID(ctx, text.Language)
|
existingLoginText, err := c.defaultLoginTextWriteModelByID(ctx, text.Language)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -33,20 +33,54 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) {
|
|||||||
res res
|
res res
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "invalid custom login text, error",
|
name: "empty custom login text, success",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
config: &domain.CustomLoginText{
|
||||||
|
Language: AllowedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "INSTANCE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "undefined language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
config: &domain.CustomLoginText{},
|
config: &domain.CustomLoginText{},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
config: &domain.CustomLoginText{
|
||||||
|
Language: UnsupportedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "custom login text set all fields, ok",
|
name: "custom login text set all fields, ok",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
|
@ -9,9 +9,12 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetDefaultMessageText only validates if the language is supported, not if it is allowed.
|
||||||
|
// This enables setting texts before allowing a language
|
||||||
func (c *Commands) SetDefaultMessageText(ctx context.Context, instanceID string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) {
|
func (c *Commands) SetDefaultMessageText(ctx context.Context, instanceID string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) {
|
||||||
instanceAgg := instance.NewAggregate(instanceID)
|
instanceAgg := instance.NewAggregate(instanceID)
|
||||||
events, existingMessageText, err := c.setDefaultMessageText(ctx, &instanceAgg.Aggregate, messageText)
|
events, existingMessageText, err := c.setDefaultMessageText(ctx, &instanceAgg.Aggregate, messageText)
|
||||||
@ -30,8 +33,8 @@ func (c *Commands) SetDefaultMessageText(ctx context.Context, instanceID string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) setDefaultMessageText(ctx context.Context, instanceAgg *eventstore.Aggregate, msg *domain.CustomMessageText) ([]eventstore.Command, *InstanceCustomMessageTextWriteModel, error) {
|
func (c *Commands) setDefaultMessageText(ctx context.Context, instanceAgg *eventstore.Aggregate, msg *domain.CustomMessageText) ([]eventstore.Command, *InstanceCustomMessageTextWriteModel, error) {
|
||||||
if !msg.IsValid() {
|
if err := msg.IsValid(i18n.SupportedLanguages()); err != nil {
|
||||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid")
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingMessageText, err := c.defaultCustomMessageTextWriteModelByID(ctx, msg.MessageTextType, msg.Language)
|
existingMessageText, err := c.defaultCustomMessageTextWriteModelByID(ctx, msg.MessageTextType, msg.Language)
|
||||||
@ -129,8 +132,8 @@ func prepareSetInstanceCustomMessageTexts(
|
|||||||
msg *domain.CustomMessageText,
|
msg *domain.CustomMessageText,
|
||||||
) preparation.Validation {
|
) preparation.Validation {
|
||||||
return func() (preparation.CreateCommands, error) {
|
return func() (preparation.CreateCommands, error) {
|
||||||
if !msg.IsValid() {
|
if err := msg.IsValid(i18n.SupportedLanguages()); err != nil {
|
||||||
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid")
|
return nil, err
|
||||||
}
|
}
|
||||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||||
existing, err := existingInstanceCustomMessageText(ctx, filter, msg.MessageTextType, msg.Language)
|
existing, err := existingInstanceCustomMessageText(ctx, filter, msg.MessageTextType, msg.Language)
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
zitadel_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
)
|
)
|
||||||
@ -34,19 +34,68 @@ func TestCommandSide_SetDefaultMessageText(t *testing.T) {
|
|||||||
res res
|
res res
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "invalid custom text, error",
|
name: "empty message type, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
config: &domain.CustomMessageText{
|
||||||
|
Language: AllowedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty custom message text, success",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
instanceID: "INSTANCE",
|
config: &domain.CustomMessageText{
|
||||||
config: &domain.CustomMessageText{},
|
MessageTextType: "Some type", // TODO: check the type!
|
||||||
|
Language: AllowedLanguage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "INSTANCE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "undefined language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
config: &domain.CustomMessageText{},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
config: &domain.CustomMessageText{
|
||||||
|
Language: UnsupportedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -212,7 +212,7 @@ func (m *mockInstance) ConsoleApplicationID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockInstance) DefaultLanguage() language.Tag {
|
func (m *mockInstance) DefaultLanguage() language.Tag {
|
||||||
return language.English
|
return AllowedLanguage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockInstance) DefaultOrganisationID() string {
|
func (m *mockInstance) DefaultOrganisationID() string {
|
||||||
|
@ -8,9 +8,12 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetOrgLoginText only validates if the language is supported, not if it is allowed.
|
||||||
|
// This enables setting texts before allowing a language
|
||||||
func (c *Commands) SetOrgLoginText(ctx context.Context, resourceOwner string, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) {
|
func (c *Commands) SetOrgLoginText(ctx context.Context, resourceOwner string, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) {
|
||||||
if resourceOwner == "" {
|
if resourceOwner == "" {
|
||||||
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-m29rF", "Errors.ResourceOwnerMissing")
|
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-m29rF", "Errors.ResourceOwnerMissing")
|
||||||
@ -32,10 +35,9 @@ func (c *Commands) SetOrgLoginText(ctx context.Context, resourceOwner string, lo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) setOrgLoginText(ctx context.Context, orgAgg *eventstore.Aggregate, loginText *domain.CustomLoginText) ([]eventstore.Command, *OrgCustomLoginTextReadModel, error) {
|
func (c *Commands) setOrgLoginText(ctx context.Context, orgAgg *eventstore.Aggregate, loginText *domain.CustomLoginText) ([]eventstore.Command, *OrgCustomLoginTextReadModel, error) {
|
||||||
if !loginText.IsValid() {
|
if err := loginText.IsValid(i18n.SupportedLanguages()); err != nil {
|
||||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "ORG-PPo2w", "Errors.CustomText.Invalid")
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingLoginText, err := c.orgCustomLoginTextWriteModelByID(ctx, orgAgg.ID, loginText.Language)
|
existingLoginText, err := c.orgCustomLoginTextWriteModelByID(ctx, orgAgg.ID, loginText.Language)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
@ -40,22 +41,44 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: authz.WithInstanceID(context.Background(), "org1"),
|
||||||
config: &domain.CustomLoginText{},
|
config: &domain.CustomLoginText{
|
||||||
|
Language: AllowedLanguage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid custom login text, error",
|
name: "empty custom login text, success",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: authz.WithInstanceID(context.Background(), "org1"),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
config: &domain.CustomLoginText{
|
||||||
|
Language: AllowedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "undefined language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "org1"),
|
||||||
resourceOwner: "org1",
|
resourceOwner: "org1",
|
||||||
config: &domain.CustomLoginText{},
|
config: &domain.CustomLoginText{},
|
||||||
},
|
},
|
||||||
@ -63,6 +86,22 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) {
|
|||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "org1"),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
config: &domain.CustomLoginText{
|
||||||
|
Language: UnsupportedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "custom login text set all fields, ok",
|
name: "custom login text set all fields, ok",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
|
@ -8,9 +8,12 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetOrgMessageText only validates if the language is supported, not if it is allowed.
|
||||||
|
// This enables setting texts before allowing a language
|
||||||
func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) {
|
func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) {
|
||||||
if resourceOwner == "" {
|
if resourceOwner == "" {
|
||||||
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2biiR", "Errors.ResourceOwnerMissing")
|
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2biiR", "Errors.ResourceOwnerMissing")
|
||||||
@ -32,10 +35,9 @@ func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) setOrgMessageText(ctx context.Context, orgAgg *eventstore.Aggregate, message *domain.CustomMessageText) ([]eventstore.Command, *OrgCustomMessageTextReadModel, error) {
|
func (c *Commands) setOrgMessageText(ctx context.Context, orgAgg *eventstore.Aggregate, message *domain.CustomMessageText) ([]eventstore.Command, *OrgCustomMessageTextReadModel, error) {
|
||||||
if !message.IsValid() {
|
if err := message.IsValid(i18n.SupportedLanguages()); err != nil {
|
||||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2jfsf", "Errors.CustomText.Invalid")
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingMessageText, err := c.orgCustomMessageTextWriteModelByID(ctx, orgAgg.ID, message.MessageTextType, message.Language)
|
existingMessageText, err := c.orgCustomMessageTextWriteModelByID(ctx, orgAgg.ID, message.MessageTextType, message.Language)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
zitadel_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
)
|
)
|
||||||
@ -35,32 +35,83 @@ func TestCommandSide_SetCustomMessageText(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "no resource owner, error",
|
name: "no resource owner, error",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(t),
|
||||||
t,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
config: &domain.CustomMessageText{},
|
config: &domain.CustomMessageText{},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid custom text, error",
|
name: "empty message type, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
config: &domain.CustomMessageText{
|
||||||
|
Language: AllowedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty custom message text, success",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
config: &domain.CustomMessageText{
|
||||||
|
MessageTextType: "Some type", // TODO: check the type!
|
||||||
|
Language: AllowedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "undefined language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
resourceOwner: "org1",
|
resourceOwner: "org1",
|
||||||
config: &domain.CustomMessageText{},
|
config: &domain.CustomMessageText{},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported language, error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
config: &domain.CustomMessageText{
|
||||||
|
Language: UnsupportedLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -345,7 +396,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) {
|
|||||||
lang: language.English,
|
lang: language.English,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -361,7 +412,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) {
|
|||||||
lang: language.English,
|
lang: language.English,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -377,7 +428,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) {
|
|||||||
mailTextType: "Template",
|
mailTextType: "Template",
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsErrorInvalidArgument,
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -471,6 +522,43 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "remove unsupported language ok, especially because we never validated whether a language is supported in previous ZITADEL versions",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewCustomTextSetEvent(context.Background(),
|
||||||
|
&org.NewAggregate("org1").Aggregate,
|
||||||
|
"Template",
|
||||||
|
domain.MessageGreeting,
|
||||||
|
"Greeting",
|
||||||
|
UnsupportedLanguage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
org.NewCustomTextTemplateRemovedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("org1").Aggregate,
|
||||||
|
"Template",
|
||||||
|
UnsupportedLanguage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
mailTextType: "Template",
|
||||||
|
lang: UnsupportedLanguage,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -141,7 +141,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
|
|||||||
"lastname1",
|
"lastname1",
|
||||||
"nickname1",
|
"nickname1",
|
||||||
"displayname1",
|
"displayname1",
|
||||||
language.German,
|
language.English,
|
||||||
domain.GenderMale,
|
domain.GenderMale,
|
||||||
"email1",
|
"email1",
|
||||||
true,
|
true,
|
||||||
@ -185,7 +185,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
|
|||||||
"lastname1",
|
"lastname1",
|
||||||
"nickname1",
|
"nickname1",
|
||||||
"displayname1",
|
"displayname1",
|
||||||
language.German,
|
language.English,
|
||||||
domain.GenderMale,
|
domain.GenderMale,
|
||||||
"email1",
|
"email1",
|
||||||
true,
|
true,
|
||||||
@ -253,7 +253,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
|
|||||||
"lastname1",
|
"lastname1",
|
||||||
"nickname1",
|
"nickname1",
|
||||||
"displayname1",
|
"displayname1",
|
||||||
language.German,
|
language.English,
|
||||||
domain.GenderMale,
|
domain.GenderMale,
|
||||||
"email1",
|
"email1",
|
||||||
true,
|
true,
|
||||||
@ -321,7 +321,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
|
|||||||
"lastname1",
|
"lastname1",
|
||||||
"nickname1",
|
"nickname1",
|
||||||
"displayname1",
|
"displayname1",
|
||||||
language.German,
|
language.English,
|
||||||
domain.GenderMale,
|
domain.GenderMale,
|
||||||
"email1",
|
"email1",
|
||||||
true,
|
true,
|
||||||
@ -392,7 +392,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
|
|||||||
"lastname1",
|
"lastname1",
|
||||||
"nickname1",
|
"nickname1",
|
||||||
"displayname1",
|
"displayname1",
|
||||||
language.German,
|
language.English,
|
||||||
domain.GenderMale,
|
domain.GenderMale,
|
||||||
"email1",
|
"email1",
|
||||||
true,
|
true,
|
||||||
@ -1181,7 +1181,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
|
|||||||
"lastname1",
|
"lastname1",
|
||||||
"nickname1",
|
"nickname1",
|
||||||
"displayname1",
|
"displayname1",
|
||||||
language.German,
|
language.English,
|
||||||
domain.GenderMale,
|
domain.GenderMale,
|
||||||
"email1",
|
"email1",
|
||||||
false,
|
false,
|
||||||
|
@ -3,16 +3,38 @@ package command
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SetRestrictions struct {
|
type SetRestrictions struct {
|
||||||
DisallowPublicOrgRegistration *bool
|
DisallowPublicOrgRegistration *bool
|
||||||
|
AllowedLanguages []language.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SetRestrictions) Validate(defaultLanguage language.Tag) error {
|
||||||
|
if s == nil || (s.DisallowPublicOrgRegistration == nil && s.AllowedLanguages == nil) {
|
||||||
|
return zitadel_errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified")
|
||||||
|
}
|
||||||
|
if s.AllowedLanguages != nil {
|
||||||
|
if err := domain.LanguagesHaveDuplicates(s.AllowedLanguages); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := domain.LanguagesAreSupported(i18n.SupportedLanguages(), s.AllowedLanguages...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := domain.LanguageIsAllowed(false, s.AllowedLanguages, defaultLanguage); err != nil {
|
||||||
|
return zitadel_errors.ThrowPreconditionFailedf(err, "COMMAND-L0m2u", "Errors.Restrictions.DefaultLanguageMustBeAllowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRestrictions creates new restrictions or updates existing restrictions.
|
// SetRestrictions creates new restrictions or updates existing restrictions.
|
||||||
@ -60,10 +82,10 @@ func (c *Commands) getRestrictionsWriteModel(ctx context.Context, instanceId, re
|
|||||||
|
|
||||||
func (c *Commands) SetRestrictionsCommand(a *restrictions.Aggregate, wm *restrictionsWriteModel, setRestrictions *SetRestrictions) preparation.Validation {
|
func (c *Commands) SetRestrictionsCommand(a *restrictions.Aggregate, wm *restrictionsWriteModel, setRestrictions *SetRestrictions) preparation.Validation {
|
||||||
return func() (preparation.CreateCommands, error) {
|
return func() (preparation.CreateCommands, error) {
|
||||||
if setRestrictions == nil || setRestrictions.DisallowPublicOrgRegistration == nil {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified")
|
|
||||||
}
|
|
||||||
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||||
|
if err := setRestrictions.Validate(authz.GetInstance(ctx).DefaultLanguage()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
changes := wm.NewChanges(setRestrictions)
|
changes := wm.NewChanges(setRestrictions)
|
||||||
if len(changes) == 0 {
|
if len(changes) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type restrictionsWriteModel struct {
|
type restrictionsWriteModel struct {
|
||||||
eventstore.WriteModel
|
eventstore.WriteModel
|
||||||
disallowPublicOrgRegistrations bool
|
disallowPublicOrgRegistration bool
|
||||||
|
allowedLanguages []language.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRestrictionsWriteModel aggregateId is filled by reducing unit matching events
|
// newRestrictionsWriteModel aggregateId is filled by reducing unit matching events
|
||||||
@ -34,8 +38,15 @@ func (wm *restrictionsWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
func (wm *restrictionsWriteModel) Reduce() error {
|
func (wm *restrictionsWriteModel) Reduce() error {
|
||||||
for _, event := range wm.Events {
|
for _, event := range wm.Events {
|
||||||
wm.ChangeDate = event.CreatedAt()
|
wm.ChangeDate = event.CreatedAt()
|
||||||
if e, ok := event.(*restrictions.SetEvent); ok && e.DisallowPublicOrgRegistrations != nil {
|
e, ok := event.(*restrictions.SetEvent)
|
||||||
wm.disallowPublicOrgRegistrations = *e.DisallowPublicOrgRegistrations
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.DisallowPublicOrgRegistration != nil {
|
||||||
|
wm.disallowPublicOrgRegistration = *e.DisallowPublicOrgRegistration
|
||||||
|
}
|
||||||
|
if e.AllowedLanguages != nil {
|
||||||
|
wm.allowedLanguages = *e.AllowedLanguages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return wm.WriteModel.Reduce()
|
return wm.WriteModel.Reduce()
|
||||||
@ -48,8 +59,11 @@ func (wm *restrictionsWriteModel) NewChanges(setRestrictions *SetRestrictions) (
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
changes = make([]restrictions.RestrictionsChange, 0, 1)
|
changes = make([]restrictions.RestrictionsChange, 0, 1)
|
||||||
if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistrations != *setRestrictions.DisallowPublicOrgRegistration) {
|
if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistration != *setRestrictions.DisallowPublicOrgRegistration) {
|
||||||
changes = append(changes, restrictions.ChangePublicOrgRegistrations(*setRestrictions.DisallowPublicOrgRegistration))
|
changes = append(changes, restrictions.ChangeDisallowPublicOrgRegistration(*setRestrictions.DisallowPublicOrgRegistration))
|
||||||
|
}
|
||||||
|
if setRestrictions.AllowedLanguages != nil && domain.LanguagesDiffer(wm.allowedLanguages, setRestrictions.AllowedLanguages) {
|
||||||
|
changes = append(changes, restrictions.ChangeAllowedLanguages(setRestrictions.AllowedLanguages))
|
||||||
}
|
}
|
||||||
return changes
|
return changes
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
@ -19,7 +20,6 @@ import (
|
|||||||
func TestSetRestrictions(t *testing.T) {
|
func TestSetRestrictions(t *testing.T) {
|
||||||
type fields func(*testing.T) (*eventstore.Eventstore, id.Generator)
|
type fields func(*testing.T) (*eventstore.Eventstore, id.Generator)
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
|
||||||
setRestrictions *SetRestrictions
|
setRestrictions *SetRestrictions
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
@ -40,14 +40,14 @@ func TestSetRestrictions(t *testing.T) {
|
|||||||
expectFilter(),
|
expectFilter(),
|
||||||
expectPush(
|
expectPush(
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"instance1",
|
"INSTANCE",
|
||||||
restrictions.NewSetEvent(
|
restrictions.NewSetEvent(
|
||||||
eventstore.NewBaseEventForPush(
|
eventstore.NewBaseEventForPush(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
&restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate,
|
||||||
restrictions.SetEventType,
|
restrictions.SetEventType,
|
||||||
),
|
),
|
||||||
restrictions.ChangePublicOrgRegistrations(true),
|
restrictions.ChangeDisallowPublicOrgRegistration(true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -55,14 +55,13 @@ func TestSetRestrictions(t *testing.T) {
|
|||||||
id_mock.NewIDGeneratorExpectIDs(t, "restrictions1")
|
id_mock.NewIDGeneratorExpectIDs(t, "restrictions1")
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
|
||||||
setRestrictions: &SetRestrictions{
|
setRestrictions: &SetRestrictions{
|
||||||
DisallowPublicOrgRegistration: gu.Ptr(true),
|
DisallowPublicOrgRegistration: gu.Ptr(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.ObjectDetails{
|
want: &domain.ObjectDetails{
|
||||||
ResourceOwner: "instance1",
|
ResourceOwner: "INSTANCE",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -76,23 +75,23 @@ func TestSetRestrictions(t *testing.T) {
|
|||||||
restrictions.NewSetEvent(
|
restrictions.NewSetEvent(
|
||||||
eventstore.NewBaseEventForPush(
|
eventstore.NewBaseEventForPush(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
&restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate,
|
||||||
restrictions.SetEventType,
|
restrictions.SetEventType,
|
||||||
),
|
),
|
||||||
restrictions.ChangePublicOrgRegistrations(true),
|
restrictions.ChangeDisallowPublicOrgRegistration(true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
expectPush(
|
expectPush(
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"instance1",
|
"INSTANCE",
|
||||||
restrictions.NewSetEvent(
|
restrictions.NewSetEvent(
|
||||||
eventstore.NewBaseEventForPush(
|
eventstore.NewBaseEventForPush(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
&restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate,
|
||||||
restrictions.SetEventType,
|
restrictions.SetEventType,
|
||||||
),
|
),
|
||||||
restrictions.ChangePublicOrgRegistrations(false),
|
restrictions.ChangeDisallowPublicOrgRegistration(false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -100,14 +99,13 @@ func TestSetRestrictions(t *testing.T) {
|
|||||||
nil
|
nil
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
|
||||||
setRestrictions: &SetRestrictions{
|
setRestrictions: &SetRestrictions{
|
||||||
DisallowPublicOrgRegistration: gu.Ptr(false),
|
DisallowPublicOrgRegistration: gu.Ptr(false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.ObjectDetails{
|
want: &domain.ObjectDetails{
|
||||||
ResourceOwner: "instance1",
|
ResourceOwner: "INSTANCE",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -121,10 +119,10 @@ func TestSetRestrictions(t *testing.T) {
|
|||||||
restrictions.NewSetEvent(
|
restrictions.NewSetEvent(
|
||||||
eventstore.NewBaseEventForPush(
|
eventstore.NewBaseEventForPush(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
&restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate,
|
||||||
restrictions.SetEventType,
|
restrictions.SetEventType,
|
||||||
),
|
),
|
||||||
restrictions.ChangePublicOrgRegistrations(true),
|
restrictions.ChangeDisallowPublicOrgRegistration(true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -132,14 +130,13 @@ func TestSetRestrictions(t *testing.T) {
|
|||||||
nil
|
nil
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
|
||||||
setRestrictions: &SetRestrictions{
|
setRestrictions: &SetRestrictions{
|
||||||
DisallowPublicOrgRegistration: gu.Ptr(true),
|
DisallowPublicOrgRegistration: gu.Ptr(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.ObjectDetails{
|
want: &domain.ObjectDetails{
|
||||||
ResourceOwner: "instance1",
|
ResourceOwner: "INSTANCE",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -152,29 +149,82 @@ func TestSetRestrictions(t *testing.T) {
|
|||||||
restrictions.NewSetEvent(
|
restrictions.NewSetEvent(
|
||||||
eventstore.NewBaseEventForPush(
|
eventstore.NewBaseEventForPush(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
&restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate,
|
||||||
restrictions.SetEventType,
|
restrictions.SetEventType,
|
||||||
),
|
),
|
||||||
restrictions.ChangePublicOrgRegistrations(true),
|
restrictions.ChangeDisallowPublicOrgRegistration(true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
), nil
|
), nil
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
|
||||||
setRestrictions: &SetRestrictions{},
|
setRestrictions: &SetRestrictions{},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: zitadel_errs.IsErrorInvalidArgument,
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported language restricted",
|
||||||
|
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
|
||||||
|
return eventstoreExpect(t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
restrictions.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
&restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate,
|
||||||
|
restrictions.SetEventType,
|
||||||
|
),
|
||||||
|
restrictions.ChangeAllowedLanguages(SupportedLanguages),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), nil
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
setRestrictions: &SetRestrictions{
|
||||||
|
AllowedLanguages: []language.Tag{AllowedLanguage, UnsupportedLanguage},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: zitadel_errs.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default language not allowed",
|
||||||
|
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
|
||||||
|
return eventstoreExpect(t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
restrictions.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
&restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate,
|
||||||
|
restrictions.SetEventType,
|
||||||
|
),
|
||||||
|
restrictions.ChangeAllowedLanguages(OnlyAllowedLanguages),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), nil
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
setRestrictions: &SetRestrictions{
|
||||||
|
AllowedLanguages: []language.Tag{DisallowedLanguage},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: zitadel_errs.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := new(Commands)
|
r := new(Commands)
|
||||||
r.eventstore, r.idGenerator = tt.fields(t)
|
r.eventstore, r.idGenerator = tt.fields(t)
|
||||||
got, err := r.SetInstanceRestrictions(tt.args.ctx, tt.args.setRestrictions)
|
got, err := r.SetInstanceRestrictions(authz.WithInstance(context.Background(), &mockInstance{}), tt.args.setRestrictions)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
zitadel_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
"github.com/zitadel/zitadel/internal/repository/user"
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
@ -36,8 +36,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "user not existing, precondition error",
|
name: "user not existing, precondition error",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(t,
|
||||||
t,
|
|
||||||
expectFilter(),
|
expectFilter(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -51,13 +50,13 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
LastName: "lastname",
|
LastName: "lastname",
|
||||||
NickName: "nickname",
|
NickName: "nickname",
|
||||||
DisplayName: "displayname",
|
DisplayName: "displayname",
|
||||||
PreferredLanguage: language.German,
|
PreferredLanguage: AllowedLanguage,
|
||||||
Gender: domain.GenderFemale,
|
Gender: domain.GenderFemale,
|
||||||
},
|
},
|
||||||
resourceOwner: "org1",
|
resourceOwner: "org1",
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsPreconditionFailed,
|
err: zitadel_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -74,7 +73,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
"lastname",
|
"lastname",
|
||||||
"nickname",
|
"nickname",
|
||||||
"displayname",
|
"displayname",
|
||||||
language.German,
|
AllowedLanguage,
|
||||||
domain.GenderFemale,
|
domain.GenderFemale,
|
||||||
"email",
|
"email",
|
||||||
true,
|
true,
|
||||||
@ -93,13 +92,13 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
LastName: "lastname",
|
LastName: "lastname",
|
||||||
NickName: "nickname",
|
NickName: "nickname",
|
||||||
DisplayName: "displayname",
|
DisplayName: "displayname",
|
||||||
PreferredLanguage: language.German,
|
PreferredLanguage: AllowedLanguage,
|
||||||
Gender: domain.GenderFemale,
|
Gender: domain.GenderFemale,
|
||||||
},
|
},
|
||||||
resourceOwner: "org1",
|
resourceOwner: "org1",
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: caos_errs.IsPreconditionFailed,
|
err: zitadel_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -116,7 +115,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
"lastname",
|
"lastname",
|
||||||
"nickname",
|
"nickname",
|
||||||
"displayname",
|
"displayname",
|
||||||
language.German,
|
DisallowedLanguage,
|
||||||
domain.GenderUnspecified,
|
domain.GenderUnspecified,
|
||||||
"email",
|
"email",
|
||||||
true,
|
true,
|
||||||
@ -130,7 +129,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
"lastname2",
|
"lastname2",
|
||||||
"nickname2",
|
"nickname2",
|
||||||
"displayname2",
|
"displayname2",
|
||||||
language.English,
|
AllowedLanguage,
|
||||||
domain.GenderMale,
|
domain.GenderMale,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -146,7 +145,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
LastName: "lastname2",
|
LastName: "lastname2",
|
||||||
NickName: "nickname2",
|
NickName: "nickname2",
|
||||||
DisplayName: "displayname2",
|
DisplayName: "displayname2",
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: AllowedLanguage,
|
||||||
Gender: domain.GenderMale,
|
Gender: domain.GenderMale,
|
||||||
},
|
},
|
||||||
resourceOwner: "org1",
|
resourceOwner: "org1",
|
||||||
@ -161,7 +160,133 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) {
|
|||||||
LastName: "lastname2",
|
LastName: "lastname2",
|
||||||
NickName: "nickname2",
|
NickName: "nickname2",
|
||||||
DisplayName: "displayname2",
|
DisplayName: "displayname2",
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: AllowedLanguage,
|
||||||
|
Gender: domain.GenderMale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "undefined preferred language, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
DisallowedLanguage,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
newProfileChangedEvent(context.Background(),
|
||||||
|
"user1", "org1",
|
||||||
|
"firstname2",
|
||||||
|
"lastname2",
|
||||||
|
"nickname2",
|
||||||
|
"displayname2",
|
||||||
|
language.Und,
|
||||||
|
domain.GenderMale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
address: &domain.Profile{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
},
|
||||||
|
FirstName: "firstname2",
|
||||||
|
LastName: "lastname2",
|
||||||
|
NickName: "nickname2",
|
||||||
|
DisplayName: "displayname2",
|
||||||
|
Gender: domain.GenderMale,
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.Profile{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
FirstName: "firstname2",
|
||||||
|
LastName: "lastname2",
|
||||||
|
NickName: "nickname2",
|
||||||
|
DisplayName: "displayname2",
|
||||||
|
PreferredLanguage: language.Und,
|
||||||
|
Gender: domain.GenderMale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "unsupported preferred language, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
DisallowedLanguage,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
newProfileChangedEvent(context.Background(),
|
||||||
|
"user1", "org1",
|
||||||
|
"firstname2",
|
||||||
|
"lastname2",
|
||||||
|
"nickname2",
|
||||||
|
"displayname2",
|
||||||
|
UnsupportedLanguage,
|
||||||
|
domain.GenderMale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
address: &domain.Profile{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
},
|
||||||
|
FirstName: "firstname2",
|
||||||
|
LastName: "lastname2",
|
||||||
|
NickName: "nickname2",
|
||||||
|
DisplayName: "displayname2",
|
||||||
|
PreferredLanguage: UnsupportedLanguage,
|
||||||
|
Gender: domain.GenderMale,
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.Profile{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
FirstName: "firstname2",
|
||||||
|
LastName: "lastname2",
|
||||||
|
NickName: "nickname2",
|
||||||
|
DisplayName: "displayname2",
|
||||||
|
PreferredLanguage: UnsupportedLanguage,
|
||||||
Gender: domain.GenderMale,
|
Gender: domain.GenderMale,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -5,11 +5,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/text/language"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TagToLanguageHookFunc() mapstructure.DecodeHookFuncType {
|
func TagToLanguageHookFunc() mapstructure.DecodeHookFuncType {
|
||||||
@ -21,6 +23,7 @@ func TagToLanguageHookFunc() mapstructure.DecodeHookFuncType {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return language.Parse(data.(string))
|
lang, err := domain.ParseLanguage(data.(string))
|
||||||
|
return lang[0], err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,8 +343,11 @@ type CustomLoginText struct {
|
|||||||
Footer FooterText
|
Footer FooterText
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CustomLoginText) IsValid() bool {
|
func (m *CustomLoginText) IsValid(supportedLanguages []language.Tag) error {
|
||||||
return m.Language != language.Und
|
if err := LanguageIsDefined(m.Language); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return LanguagesAreSupported(supportedLanguages, m.Language)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectAccountScreenText struct {
|
type SelectAccountScreenText struct {
|
||||||
|
@ -3,6 +3,7 @@ package domain
|
|||||||
import (
|
import (
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
zitadel_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,8 +52,14 @@ type CustomMessageText struct {
|
|||||||
FooterText string
|
FooterText string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CustomMessageText) IsValid() bool {
|
func (m *CustomMessageText) IsValid(supportedLanguages []language.Tag) error {
|
||||||
return m.MessageTextType != "" && m.Language != language.Und
|
if m.MessageTextType == "" {
|
||||||
|
return zitadel_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid")
|
||||||
|
}
|
||||||
|
if err := LanguageIsDefined(m.Language); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return LanguagesAreSupported(supportedLanguages, m.Language)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsMessageTextType(textType string) bool {
|
func IsMessageTextType(textType string) bool {
|
||||||
|
130
internal/domain/language.go
Normal file
130
internal/domain/language.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
z_errors "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StringsToLanguages(langs []string) []language.Tag {
|
||||||
|
return GenericMapSlice(langs, language.Make)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LanguagesToStrings(langs []language.Tag) []string {
|
||||||
|
return GenericMapSlice(langs, func(lang language.Tag) string { return lang.String() })
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenericMapSlice[T any, U any](from []T, mapTo func(T) U) []U {
|
||||||
|
if from == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]U, len(from))
|
||||||
|
for i, lang := range from {
|
||||||
|
result[i] = mapTo(lang)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// LanguagesDiffer returns true if the languages differ.
|
||||||
|
func LanguagesDiffer(left, right []language.Tag) bool {
|
||||||
|
if left == nil && right == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if left == nil || right == nil || len(left) != len(right) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !languagesAreContained(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LanguageIsAllowed(allowUndefined bool, allowedLanguages []language.Tag, lang language.Tag) error {
|
||||||
|
err := LanguageIsDefined(lang)
|
||||||
|
if err != nil && allowUndefined {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(allowedLanguages) > 0 && !languageIsContained(allowedLanguages, lang) {
|
||||||
|
return z_errors.ThrowPreconditionFailed(nil, "LANG-2M9fs", "Errors.Language.NotAllowed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LanguagesAreSupported(supportedLanguages []language.Tag, lang ...language.Tag) error {
|
||||||
|
unsupported := make([]language.Tag, 0)
|
||||||
|
for _, l := range lang {
|
||||||
|
if l.IsRoot() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !languageIsContained(supportedLanguages, l) {
|
||||||
|
unsupported = append(unsupported, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(unsupported) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(unsupported) == 1 {
|
||||||
|
return z_errors.ThrowInvalidArgument(nil, "LANG-lg4DP", "Errors.Language.NotSupported")
|
||||||
|
}
|
||||||
|
return z_errors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.NotSupported: %s", LanguagesToStrings(unsupported))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LanguageIsDefined(lang language.Tag) error {
|
||||||
|
if lang.IsRoot() {
|
||||||
|
return z_errors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Undefined")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LanguagesHaveDuplicates returns an error if the passed slices contains duplicates.
|
||||||
|
// The error lists the duplicates.
|
||||||
|
func LanguagesHaveDuplicates(langs []language.Tag) error {
|
||||||
|
unique := make(map[language.Tag]struct{})
|
||||||
|
duplicates := make([]language.Tag, 0)
|
||||||
|
for _, lang := range langs {
|
||||||
|
if _, ok := unique[lang]; ok {
|
||||||
|
duplicates = append(duplicates, lang)
|
||||||
|
}
|
||||||
|
unique[lang] = struct{}{}
|
||||||
|
}
|
||||||
|
if len(duplicates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(duplicates) > 1 {
|
||||||
|
return z_errors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Duplicate")
|
||||||
|
}
|
||||||
|
return z_errors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.Duplicate: %s", LanguagesToStrings(duplicates))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseLanguage(lang ...string) (tags []language.Tag, err error) {
|
||||||
|
tags = make([]language.Tag, len(lang))
|
||||||
|
for i := range lang {
|
||||||
|
var parseErr error
|
||||||
|
tags[i], parseErr = language.Parse(lang[i])
|
||||||
|
err = errors.Join(err, parseErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
err = z_errors.ThrowInvalidArgument(err, "LANG-jc8Sq", "Errors.Language.NotParsed")
|
||||||
|
}
|
||||||
|
return tags, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func languagesAreContained(languages, search []language.Tag) bool {
|
||||||
|
for _, s := range search {
|
||||||
|
if !languageIsContained(languages, s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func languageIsContained(languages []language.Tag, search language.Tag) bool {
|
||||||
|
for _, lang := range languages {
|
||||||
|
if lang == search {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -55,6 +55,7 @@ func AggregateFromWriteModel(
|
|||||||
version Version,
|
version Version,
|
||||||
) *Aggregate {
|
) *Aggregate {
|
||||||
return NewAggregate(
|
return NewAggregate(
|
||||||
|
// TODO: the linter complains if this function is called without passing a context
|
||||||
context.Background(),
|
context.Background(),
|
||||||
wm.AggregateID,
|
wm.AggregateID,
|
||||||
typ,
|
typ,
|
||||||
|
60
internal/i18n/bundle.go
Normal file
60
internal/i18n/bundle.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const i18nPath = "/i18n"
|
||||||
|
|
||||||
|
func newBundle(dir http.FileSystem, defaultLanguage language.Tag, allowedLanguages []language.Tag) (*i18n.Bundle, error) {
|
||||||
|
bundle := i18n.NewBundle(defaultLanguage)
|
||||||
|
bundle.RegisterUnmarshalFunc("yaml", func(data []byte, v interface{}) error { return yaml.Unmarshal(data, v) })
|
||||||
|
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||||
|
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||||
|
i18nDir, err := dir.Open(i18nPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, zitadel_errors.ThrowNotFound(err, "I18N-MnXRie", "path not found")
|
||||||
|
}
|
||||||
|
defer i18nDir.Close()
|
||||||
|
files, err := i18nDir.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, zitadel_errors.ThrowNotFound(err, "I18N-Gew23", "cannot read dir")
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
fileLang, _ := strings.CutSuffix(file.Name(), filepath.Ext(file.Name()))
|
||||||
|
if err = domain.LanguageIsAllowed(false, allowedLanguages, language.Make(fileLang)); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := addFileFromFileSystemToBundle(dir, bundle, file); err != nil {
|
||||||
|
return nil, zitadel_errors.ThrowNotFoundf(err, "I18N-ZS2AW", "cannot append file %s to Bundle", file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bundle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFileFromFileSystemToBundle(dir http.FileSystem, bundle *i18n.Bundle, file os.FileInfo) error {
|
||||||
|
f, err := dir.Open("/i18n/" + file.Name())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
content, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = bundle.ParseMessageFileBytes(content, file.Name())
|
||||||
|
return err
|
||||||
|
}
|
48
internal/i18n/fs.go
Normal file
48
internal/i18n/fs.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rakyll/statik/fs"
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
var zitadelFS, loginFS, notificationFS http.FileSystem
|
||||||
|
|
||||||
|
type Namespace string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ZITADEL Namespace = "zitadel"
|
||||||
|
LOGIN Namespace = "login"
|
||||||
|
NOTIFICATION Namespace = "notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadFilesystem(ns Namespace) http.FileSystem {
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
logging.WithFields("namespace", ns).OnError(err).Panic("unable to get namespace")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
switch ns {
|
||||||
|
case ZITADEL:
|
||||||
|
if zitadelFS != nil {
|
||||||
|
return zitadelFS
|
||||||
|
}
|
||||||
|
zitadelFS, err = fs.NewWithNamespace(string(ns))
|
||||||
|
return zitadelFS
|
||||||
|
case LOGIN:
|
||||||
|
if loginFS != nil {
|
||||||
|
return loginFS
|
||||||
|
}
|
||||||
|
loginFS, err = fs.NewWithNamespace(string(ns))
|
||||||
|
return loginFS
|
||||||
|
case NOTIFICATION:
|
||||||
|
if notificationFS != nil {
|
||||||
|
return notificationFS
|
||||||
|
}
|
||||||
|
notificationFS, err = fs.NewWithNamespace(string(ns))
|
||||||
|
return notificationFS
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
51
internal/i18n/languages.go
Normal file
51
internal/i18n/languages.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
var supportedLanguages []language.Tag
|
||||||
|
|
||||||
|
func SupportedLanguages() []language.Tag {
|
||||||
|
if supportedLanguages == nil {
|
||||||
|
panic("supported languages not loaded")
|
||||||
|
}
|
||||||
|
return supportedLanguages
|
||||||
|
}
|
||||||
|
|
||||||
|
func SupportLanguages(languages ...language.Tag) {
|
||||||
|
supportedLanguages = languages
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustLoadSupportedLanguagesFromDir() {
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to load supported languages: " + err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if supportedLanguages != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i18nDir, err := LoadFilesystem(LOGIN).Open(i18nPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = errors.Join(err, i18nDir.Close())
|
||||||
|
}()
|
||||||
|
files, err := i18nDir.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
supportedLanguages = make([]language.Tag, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
lang := language.Make(strings.TrimSuffix(file.Name(), ".yaml"))
|
||||||
|
if lang != language.Und {
|
||||||
|
supportedLanguages = append(supportedLanguages, lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,26 +2,15 @@ package i18n
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
|
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"sigs.k8s.io/yaml"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
i18nPath = "/i18n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Translator struct {
|
type Translator struct {
|
||||||
@ -29,6 +18,7 @@ type Translator struct {
|
|||||||
cookieName string
|
cookieName string
|
||||||
cookieHandler *http_util.CookieHandler
|
cookieHandler *http_util.CookieHandler
|
||||||
preferredLanguages []string
|
preferredLanguages []string
|
||||||
|
allowedLanguages []language.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
type TranslatorConfig struct {
|
type TranslatorConfig struct {
|
||||||
@ -41,10 +31,27 @@ type Message struct {
|
|||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTranslator(dir http.FileSystem, defaultLanguage language.Tag, cookieName string) (*Translator, error) {
|
// NewZitadelTranslator translates to all supported languages, as the ZITADEL texts are not customizable.
|
||||||
|
func NewZitadelTranslator(defaultLanguage language.Tag) (*Translator, error) {
|
||||||
|
return newTranslator(ZITADEL, defaultLanguage, SupportedLanguages(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationTranslator(defaultLanguage language.Tag, allowedLanguages []language.Tag) (*Translator, error) {
|
||||||
|
return newTranslator(NOTIFICATION, defaultLanguage, allowedLanguages, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoginTranslator(defaultLanguage language.Tag, allowedLanguages []language.Tag, cookieName string) (*Translator, error) {
|
||||||
|
return newTranslator(LOGIN, defaultLanguage, allowedLanguages, cookieName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTranslator(ns Namespace, defaultLanguage language.Tag, allowedLanguages []language.Tag, cookieName string) (*Translator, error) {
|
||||||
t := new(Translator)
|
t := new(Translator)
|
||||||
var err error
|
var err error
|
||||||
t.bundle, err = newBundle(dir, defaultLanguage)
|
t.allowedLanguages = allowedLanguages
|
||||||
|
if len(t.allowedLanguages) == 0 {
|
||||||
|
t.allowedLanguages = SupportedLanguages()
|
||||||
|
}
|
||||||
|
t.bundle, err = newBundle(LoadFilesystem(ns), defaultLanguage, t.allowedLanguages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -53,64 +60,8 @@ func NewTranslator(dir http.FileSystem, defaultLanguage language.Tag, cookieName
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBundle(dir http.FileSystem, defaultLanguage language.Tag) (*i18n.Bundle, error) {
|
|
||||||
bundle := i18n.NewBundle(defaultLanguage)
|
|
||||||
bundle.RegisterUnmarshalFunc("yaml", func(data []byte, v interface{}) error { return yaml.Unmarshal(data, v) })
|
|
||||||
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
|
||||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
|
||||||
i18nDir, err := dir.Open(i18nPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.ThrowNotFound(err, "I18N-MnXRie", "path not found")
|
|
||||||
}
|
|
||||||
defer i18nDir.Close()
|
|
||||||
files, err := i18nDir.Readdir(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.ThrowNotFound(err, "I18N-Gew23", "cannot read dir")
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if err := addFileFromFileSystemToBundle(dir, bundle, file); err != nil {
|
|
||||||
return nil, errors.ThrowNotFoundf(err, "I18N-ZS2AW", "cannot append file %s to Bundle", file.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bundle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addFileFromFileSystemToBundle(dir http.FileSystem, bundle *i18n.Bundle, file os.FileInfo) error {
|
|
||||||
f, err := dir.Open("/i18n/" + file.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
content, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = bundle.ParseMessageFileBytes(content, file.Name())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func SupportedLanguages(dir http.FileSystem) ([]language.Tag, error) {
|
|
||||||
i18nDir, err := dir.Open("/i18n")
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.ThrowNotFound(err, "I18N-Dbt42", "cannot open dir")
|
|
||||||
}
|
|
||||||
defer i18nDir.Close()
|
|
||||||
files, err := i18nDir.Readdir(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.ThrowNotFound(err, "I18N-Gh4zk", "cannot read dir")
|
|
||||||
}
|
|
||||||
languages := make([]language.Tag, 0, len(files))
|
|
||||||
for _, file := range files {
|
|
||||||
lang := language.Make(strings.TrimSuffix(file.Name(), ".yaml"))
|
|
||||||
if lang != language.Und {
|
|
||||||
languages = append(languages, lang)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return languages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Translator) SupportedLanguages() []language.Tag {
|
func (t *Translator) SupportedLanguages() []language.Tag {
|
||||||
return t.bundle.LanguageTags()
|
return t.allowedLanguages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Translator) AddMessages(tag language.Tag, messages ...Message) error {
|
func (t *Translator) AddMessages(tag language.Tag, messages ...Message) error {
|
||||||
@ -144,7 +95,7 @@ func (t *Translator) LocalizeWithoutArgs(id string, langs ...string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Translator) Lang(r *http.Request) language.Tag {
|
func (t *Translator) Lang(r *http.Request) language.Tag {
|
||||||
matcher := language.NewMatcher(t.bundle.LanguageTags())
|
matcher := language.NewMatcher(t.allowedLanguages)
|
||||||
tag, _ := language.MatchStrings(matcher, t.langsFromRequest(r)...)
|
tag, _ := language.MatchStrings(matcher, t.langsFromRequest(r)...)
|
||||||
return tag
|
return tag
|
||||||
}
|
}
|
@ -60,7 +60,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) {
|
func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) {
|
||||||
primaryDomain = randString(5) + ".integration.localhost"
|
primaryDomain = RandString(5) + ".integration.localhost"
|
||||||
instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{
|
instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{
|
||||||
InstanceName: "testinstance",
|
InstanceName: "testinstance",
|
||||||
CustomDomain: primaryDomain,
|
CustomDomain: primaryDomain,
|
||||||
@ -85,8 +85,8 @@ func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (pr
|
|||||||
|
|
||||||
func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse {
|
func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse {
|
||||||
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
||||||
Organisation: &object.Organisation{
|
Organization: &object.Organization{
|
||||||
Org: &object.Organisation_OrgId{
|
Org: &object.Organization_OrgId{
|
||||||
OrgId: s.Organisation.ID,
|
OrgId: s.Organisation.ID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -51,3 +51,9 @@ DefaultInstance:
|
|||||||
SystemAPIUsers:
|
SystemAPIUsers:
|
||||||
- tester:
|
- tester:
|
||||||
KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
|
KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
|
||||||
|
Memberships:
|
||||||
|
- MemberType: System
|
||||||
|
Roles:
|
||||||
|
- "SYSTEM_OWNER"
|
||||||
|
- "IAM_OWNER"
|
||||||
|
- "ORG_OWNER"
|
||||||
|
@ -11,7 +11,7 @@ func init() {
|
|||||||
|
|
||||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
|
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||||
|
|
||||||
func randString(n int) string {
|
func RandString(n int) string {
|
||||||
b := make([]rune, n)
|
b := make([]rune, n)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||||
|
@ -85,6 +85,21 @@ func (mr *MockQueriesMockRecorder) GetDefaultLanguage(arg0 any) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInstanceRestrictions mocks base method.
|
||||||
|
func (m *MockQueries) GetInstanceRestrictions(arg0 context.Context) (query.Restrictions, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetInstanceRestrictions", arg0)
|
||||||
|
ret0, _ := ret[0].(query.Restrictions)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceRestrictions indicates an expected call of GetInstanceRestrictions.
|
||||||
|
func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(arg0 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// GetNotifyUserByID mocks base method.
|
// GetNotifyUserByID mocks base method.
|
||||||
func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string, arg3 ...query.SearchQuery) (*query.NotifyUser, error) {
|
func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string, arg3 ...query.SearchQuery) (*query.NotifyUser, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -2,8 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
@ -25,6 +23,7 @@ type Queries interface {
|
|||||||
SMSProviderConfig(ctx context.Context, queries ...query.SearchQuery) (*query.SMSConfig, error)
|
SMSProviderConfig(ctx context.Context, queries ...query.SearchQuery) (*query.SMSConfig, error)
|
||||||
SMTPConfigByAggregateID(ctx context.Context, aggregateID string) (*query.SMTPConfig, error)
|
SMTPConfigByAggregateID(ctx context.Context, aggregateID string) (*query.SMTPConfig, error)
|
||||||
GetDefaultLanguage(ctx context.Context) language.Tag
|
GetDefaultLanguage(ctx context.Context) language.Tag
|
||||||
|
GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationQueries struct {
|
type NotificationQueries struct {
|
||||||
@ -37,7 +36,6 @@ type NotificationQueries struct {
|
|||||||
UserDataCrypto crypto.EncryptionAlgorithm
|
UserDataCrypto crypto.EncryptionAlgorithm
|
||||||
SMTPPasswordCrypto crypto.EncryptionAlgorithm
|
SMTPPasswordCrypto crypto.EncryptionAlgorithm
|
||||||
SMSTokenCrypto crypto.EncryptionAlgorithm
|
SMSTokenCrypto crypto.EncryptionAlgorithm
|
||||||
statikDir http.FileSystem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNotificationQueries(
|
func NewNotificationQueries(
|
||||||
@ -50,7 +48,6 @@ func NewNotificationQueries(
|
|||||||
userDataCrypto crypto.EncryptionAlgorithm,
|
userDataCrypto crypto.EncryptionAlgorithm,
|
||||||
smtpPasswordCrypto crypto.EncryptionAlgorithm,
|
smtpPasswordCrypto crypto.EncryptionAlgorithm,
|
||||||
smsTokenCrypto crypto.EncryptionAlgorithm,
|
smsTokenCrypto crypto.EncryptionAlgorithm,
|
||||||
statikDir http.FileSystem,
|
|
||||||
) *NotificationQueries {
|
) *NotificationQueries {
|
||||||
return &NotificationQueries{
|
return &NotificationQueries{
|
||||||
Queries: baseQueries,
|
Queries: baseQueries,
|
||||||
@ -62,6 +59,5 @@ func NewNotificationQueries(
|
|||||||
UserDataCrypto: userDataCrypto,
|
UserDataCrypto: userDataCrypto,
|
||||||
SMTPPasswordCrypto: smtpPasswordCrypto,
|
SMTPPasswordCrypto: smtpPasswordCrypto,
|
||||||
SMSTokenCrypto: smsTokenCrypto,
|
SMSTokenCrypto: smsTokenCrypto,
|
||||||
statikDir: statikDir,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (n *NotificationQueries) GetTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) {
|
func (n *NotificationQueries) GetTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) {
|
||||||
translator, err := i18n.NewTranslator(n.statikDir, n.GetDefaultLanguage(ctx), "")
|
restrictions, err := n.Queries.GetInstanceRestrictions(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
translator, err := i18n.NewNotificationTranslator(n.GetDefaultLanguage(ctx), restrictions.AllowedLanguages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -12,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||||
|
|
||||||
statik_fs "github.com/rakyll/statik/fs"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
@ -202,15 +200,13 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
// TODO: Why don't we have an url template on user.HumanInitialCodeAddedEvent?
|
// TODO: Why don't we have an url template on user.HumanInitialCodeAddedEvent?
|
||||||
fs, err := statik_fs.NewWithNamespace("notification")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
queries := mock.NewMockQueries(ctrl)
|
queries := mock.NewMockQueries(ctrl)
|
||||||
commands := mock.NewMockCommands(ctrl)
|
commands := mock.NewMockCommands(ctrl)
|
||||||
f, a, w := tt.test(ctrl, queries, commands)
|
f, a, w := tt.test(ctrl, queries, commands)
|
||||||
stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceInitCodeAdded(a.event)
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInitCodeAdded(a.event)
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
w.err(t, err)
|
w.err(t, err)
|
||||||
} else {
|
} else {
|
||||||
@ -423,15 +419,13 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) {
|
|||||||
}, w
|
}, w
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fs, err := statik_fs.NewWithNamespace("notification")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
queries := mock.NewMockQueries(ctrl)
|
queries := mock.NewMockQueries(ctrl)
|
||||||
commands := mock.NewMockCommands(ctrl)
|
commands := mock.NewMockCommands(ctrl)
|
||||||
f, a, w := tt.test(ctrl, queries, commands)
|
f, a, w := tt.test(ctrl, queries, commands)
|
||||||
stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceEmailCodeAdded(a.event)
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceEmailCodeAdded(a.event)
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
w.err(t, err)
|
w.err(t, err)
|
||||||
} else {
|
} else {
|
||||||
@ -644,15 +638,13 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) {
|
|||||||
}, w
|
}, w
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fs, err := statik_fs.NewWithNamespace("notification")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
queries := mock.NewMockQueries(ctrl)
|
queries := mock.NewMockQueries(ctrl)
|
||||||
commands := mock.NewMockCommands(ctrl)
|
commands := mock.NewMockCommands(ctrl)
|
||||||
f, a, w := tt.test(ctrl, queries, commands)
|
f, a, w := tt.test(ctrl, queries, commands)
|
||||||
stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordCodeAdded(a.event)
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordCodeAdded(a.event)
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
w.err(t, err)
|
w.err(t, err)
|
||||||
} else {
|
} else {
|
||||||
@ -737,15 +729,13 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) {
|
|||||||
}, w
|
}, w
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fs, err := statik_fs.NewWithNamespace("notification")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
queries := mock.NewMockQueries(ctrl)
|
queries := mock.NewMockQueries(ctrl)
|
||||||
commands := mock.NewMockCommands(ctrl)
|
commands := mock.NewMockCommands(ctrl)
|
||||||
f, a, w := tt.test(ctrl, queries, commands)
|
f, a, w := tt.test(ctrl, queries, commands)
|
||||||
stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceDomainClaimed(a.event)
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceDomainClaimed(a.event)
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
w.err(t, err)
|
w.err(t, err)
|
||||||
} else {
|
} else {
|
||||||
@ -963,15 +953,13 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) {
|
|||||||
}, w
|
}, w
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fs, err := statik_fs.NewWithNamespace("notification")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
queries := mock.NewMockQueries(ctrl)
|
queries := mock.NewMockQueries(ctrl)
|
||||||
commands := mock.NewMockCommands(ctrl)
|
commands := mock.NewMockCommands(ctrl)
|
||||||
f, a, w := tt.test(ctrl, queries, commands)
|
f, a, w := tt.test(ctrl, queries, commands)
|
||||||
stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordlessCodeRequested(a.event)
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordlessCodeRequested(a.event)
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
w.err(t, err)
|
w.err(t, err)
|
||||||
} else {
|
} else {
|
||||||
@ -1062,15 +1050,13 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) {
|
|||||||
}, w
|
}, w
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fs, err := statik_fs.NewWithNamespace("notification")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
queries := mock.NewMockQueries(ctrl)
|
queries := mock.NewMockQueries(ctrl)
|
||||||
commands := mock.NewMockCommands(ctrl)
|
commands := mock.NewMockCommands(ctrl)
|
||||||
f, a, w := tt.test(ctrl, queries, commands)
|
f, a, w := tt.test(ctrl, queries, commands)
|
||||||
stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordChanged(a.event)
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordChanged(a.event)
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
w.err(t, err)
|
w.err(t, err)
|
||||||
} else {
|
} else {
|
||||||
@ -1287,15 +1273,13 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) {
|
|||||||
}, w
|
}, w
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fs, err := statik_fs.NewWithNamespace("notification")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
queries := mock.NewMockQueries(ctrl)
|
queries := mock.NewMockQueries(ctrl)
|
||||||
commands := mock.NewMockCommands(ctrl)
|
commands := mock.NewMockCommands(ctrl)
|
||||||
f, a, w := tt.test(ctrl, queries, commands)
|
f, a, w := tt.test(ctrl, queries, commands)
|
||||||
_, err = newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceSessionOTPEmailChallenged(a.event)
|
_, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPEmailChallenged(a.event)
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
w.err(t, err)
|
w.err(t, err)
|
||||||
} else {
|
} else {
|
||||||
@ -1320,7 +1304,7 @@ type want struct {
|
|||||||
err assert.ErrorAssertionFunc
|
err assert.ErrorAssertionFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, fs http.FileSystem, f fields, a args, w want) *userNotifier {
|
func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields, a args, w want) *userNotifier {
|
||||||
queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil)
|
queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil)
|
||||||
smtpAlg, _ := cryptoValue(t, ctrl, "smtppw")
|
smtpAlg, _ := cryptoValue(t, ctrl, "smtppw")
|
||||||
channel := channel_mock.NewMockNotificationChannel(ctrl)
|
channel := channel_mock.NewMockNotificationChannel(ctrl)
|
||||||
@ -1340,7 +1324,6 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu
|
|||||||
f.userDataCrypto,
|
f.userDataCrypto,
|
||||||
smtpAlg,
|
smtpAlg,
|
||||||
f.SMSTokenCrypto,
|
f.SMSTokenCrypto,
|
||||||
fs,
|
|
||||||
),
|
),
|
||||||
otpEmailTmpl: defaultOTPEmailTemplate,
|
otpEmailTmpl: defaultOTPEmailTemplate,
|
||||||
channels: &channels{Chain: *senders.ChainChannels(channel)},
|
channels: &channels{Chain: *senders.ChainChannels(channel)},
|
||||||
@ -1366,6 +1349,9 @@ func (c *channels) Webhook(context.Context, webhook.Config) (*senders.Chain, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expectTemplateQueries(queries *mock.MockQueries, template string) {
|
func expectTemplateQueries(queries *mock.MockQueries, template string) {
|
||||||
|
queries.EXPECT().GetInstanceRestrictions(gomock.Any()).Return(query.Restrictions{
|
||||||
|
AllowedLanguages: []language.Tag{language.English},
|
||||||
|
}, nil)
|
||||||
queries.EXPECT().ActiveLabelPolicyByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.LabelPolicy{
|
queries.EXPECT().ActiveLabelPolicyByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.LabelPolicy{
|
||||||
ID: policyID,
|
ID: policyID,
|
||||||
Light: query.Theme{
|
Light: query.Theme{
|
||||||
|
@ -3,9 +3,6 @@ package notification
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
statik_fs "github.com/rakyll/statik/fs"
|
|
||||||
"github.com/zitadel/logging"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
@ -29,9 +26,7 @@ func Start(
|
|||||||
fileSystemPath string,
|
fileSystemPath string,
|
||||||
userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm,
|
userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm,
|
||||||
) {
|
) {
|
||||||
statikFS, err := statik_fs.NewWithNamespace("notification")
|
q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
|
||||||
logging.OnError(err).Panic("unable to start listener")
|
|
||||||
q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption, statikFS)
|
|
||||||
c := newChannels(q)
|
c := newChannels(q)
|
||||||
handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl).Start(ctx)
|
handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl).Start(ctx)
|
||||||
handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c).Start(ctx)
|
handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c).Start(ctx)
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/query/projection"
|
"github.com/zitadel/zitadel/internal/query/projection"
|
||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
)
|
)
|
||||||
@ -217,9 +218,9 @@ func (q *Queries) readLoginTranslationFile(ctx context.Context, lang string) ([]
|
|||||||
contents, ok := q.LoginTranslationFileContents[lang]
|
contents, ok := q.LoginTranslationFileContents[lang]
|
||||||
var err error
|
var err error
|
||||||
if !ok {
|
if !ok {
|
||||||
contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", lang))
|
contents, err = q.readTranslationFile(i18n.LOGIN, fmt.Sprintf("/i18n/%s.yaml", lang))
|
||||||
if errors.IsNotFound(err) {
|
if errors.IsNotFound(err) {
|
||||||
contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String()))
|
contents, err = q.readTranslationFile(i18n.LOGIN, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String()))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package query
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/zitadel/logging"
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/i18n"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (q *Queries) Languages(ctx context.Context) ([]language.Tag, error) {
|
|
||||||
if len(q.supportedLangs) == 0 {
|
|
||||||
langs, err := i18n.SupportedLanguages(q.LoginDir)
|
|
||||||
if err != nil {
|
|
||||||
logging.Log("ADMIN-tiMWs").WithError(err).Debug("unable to parse language")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q.supportedLangs = langs
|
|
||||||
}
|
|
||||||
return q.supportedLangs, nil
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user