fix: allow unicode characters in org domains (#6675)

Co-authored-by: Elio Bischof <eliobischof@gmail.com>
This commit is contained in:
Miguel Cabrerizo
2023-10-11 09:55:01 +02:00
committed by GitHub
parent 412cd144ef
commit 2d4cd331da
22 changed files with 79 additions and 20 deletions

View File

@@ -105,13 +105,21 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
// check if username is email style or else append @<orgname>.<custom-domain> // check if username is email style or else append @<orgname>.<custom-domain>
//this way we have the same value as before changing `UserLoginMustBeDomain` to false //this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") { if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") {
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain) orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
if err != nil {
return err
}
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + orgDomain
} }
mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Email.Address.Normalize() mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Email.Address.Normalize()
if mig.instanceSetup.Org.Human.Email.Address == "" { if mig.instanceSetup.Org.Human.Email.Address == "" {
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username) mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username)
if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") { if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") {
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)) orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
if err != nil {
return err
}
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + orgDomain)
} }
} }

View File

@@ -66,7 +66,11 @@ func (s *Server) ListOrgs(ctx context.Context, req *admin_pb.ListOrgsRequest) (*
} }
func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*admin_pb.SetUpOrgResponse, error) { func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*admin_pb.SetUpOrgResponse, error) {
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, domain.NewIAMDomainName(req.Org.Name, authz.GetInstance(ctx).RequestedDomain())) orgDomain, err := domain.NewIAMDomainName(req.Org.Name, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, orgDomain)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -72,7 +72,11 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges
} }
func (s *Server) AddOrg(ctx context.Context, req *mgmt_pb.AddOrgRequest) (*mgmt_pb.AddOrgResponse, error) { func (s *Server) AddOrg(ctx context.Context, req *mgmt_pb.AddOrgRequest) (*mgmt_pb.AddOrgResponse, error) {
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, domain.NewIAMDomainName(req.Name, authz.GetInstance(ctx).RequestedDomain()), "") orgDomain, err := domain.NewIAMDomainName(req.Name, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, orgDomain, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -79,7 +79,8 @@ func createInstancePbToAddHuman(req *system_pb.CreateInstanceRequest_Human, defa
// check if default username is email style or else append @<orgname>.<custom-domain> // check if default username is email style or else append @<orgname>.<custom-domain>
// this way we have the same value as before changing `UserLoginMustBeDomain` to false // this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !userLoginMustBeDomain && !strings.Contains(user.Username, "@") { if !userLoginMustBeDomain && !strings.Contains(user.Username, "@") {
user.Username = user.Username + "@" + domain.NewIAMDomainName(org, externalDomain) orgDomain, _ := domain.NewIAMDomainName(org, externalDomain)
user.Username = user.Username + "@" + orgDomain
} }
if req.UserName != "" { if req.UserName != "" {
user.Username = req.UserName user.Username = req.UserName
@@ -185,7 +186,8 @@ func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInst
// check if default username is email style or else append @<orgname>.<custom-domain> // check if default username is email style or else append @<orgname>.<custom-domain>
// this way we have the same value as before changing `UserLoginMustBeDomain` to false // this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !instance.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(instance.Org.Human.Username, "@") { if !instance.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(instance.Org.Human.Username, "@") {
instance.Org.Human.Username = instance.Org.Human.Username + "@" + domain.NewIAMDomainName(instance.Org.Name, externalDomain) orgDomain, _ := domain.NewIAMDomainName(instance.Org.Name, externalDomain)
instance.Org.Human.Username = instance.Org.Human.Username + "@" + orgDomain
} }
if req.OwnerPassword != nil { if req.OwnerPassword != nil {
instance.Org.Human.Password = req.OwnerPassword.Password instance.Org.Human.Password = req.OwnerPassword.Password

View File

@@ -161,7 +161,11 @@ func (l *Login) Handler() http.Handler {
} }
func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) { func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) {
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+domain.NewIAMDomainName(orgName, authz.GetInstance(ctx).RequestedDomain()), query.TextEndsWithIgnoreCase) orgDomain, err := domain.NewIAMDomainName(orgName, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -238,7 +238,10 @@ func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs .
if name = strings.TrimSpace(name); name == "" { if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument") return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")
} }
defaultDomain := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain()) defaultDomain, err := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain())
if 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) {
return []eventstore.Command{ return []eventstore.Command{
org.NewOrgAddedEvent(ctx, &a.Aggregate, name), org.NewOrgAddedEvent(ctx, &a.Aggregate, name),

View File

@@ -309,13 +309,16 @@ func (c *Commands) changeDefaultDomain(ctx context.Context, orgID, newName strin
return nil, err return nil, err
} }
iamDomain := authz.GetInstance(ctx).RequestedDomain() iamDomain := authz.GetInstance(ctx).RequestedDomain()
defaultDomain := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain) defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain)
isPrimary := defaultDomain == orgDomains.PrimaryDomain isPrimary := defaultDomain == orgDomains.PrimaryDomain
orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel)
for _, orgDomain := range orgDomains.Domains { for _, orgDomain := range orgDomains.Domains {
if orgDomain.State == domain.OrgDomainStateActive { if orgDomain.State == domain.OrgDomainStateActive {
if orgDomain.Domain == defaultDomain { if orgDomain.Domain == defaultDomain {
newDefaultDomain := domain.NewIAMDomainName(newName, iamDomain) newDefaultDomain, err := domain.NewIAMDomainName(newName, iamDomain)
if err != nil {
return nil, err
}
events := []eventstore.Command{ events := []eventstore.Command{
org.NewDomainAddedEvent(ctx, orgAgg, newDefaultDomain), org.NewDomainAddedEvent(ctx, orgAgg, newDefaultDomain),
org.NewDomainVerifiedEvent(ctx, orgAgg, newDefaultDomain), org.NewDomainVerifiedEvent(ctx, orgAgg, newDefaultDomain),
@@ -338,7 +341,7 @@ func (c *Commands) removeCustomDomains(ctx context.Context, orgID string) ([]eve
return nil, err return nil, err
} }
hasDefault := false hasDefault := false
defaultDomain := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain()) defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain())
isPrimary := defaultDomain == orgDomains.PrimaryDomain isPrimary := defaultDomain == orgDomains.PrimaryDomain
orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel)
events := make([]eventstore.Command, 0, len(orgDomains.Domains)) events := make([]eventstore.Command, 0, len(orgDomains.Domains))

View File

@@ -25,7 +25,8 @@ func (o *Org) IsValid() bool {
} }
func (o *Org) AddIAMDomain(iamDomain string) { func (o *Org) AddIAMDomain(iamDomain string) {
o.Domains = append(o.Domains, &OrgDomain{Domain: NewIAMDomainName(o.Name, iamDomain), Verified: true, Primary: true}) orgDomain, _ := NewIAMDomainName(o.Name, iamDomain)
o.Domains = append(o.Domains, &OrgDomain{Domain: orgDomain, Verified: true, Primary: true})
} }
type OrgState int32 type OrgState int32

View File

@@ -6,6 +6,7 @@ import (
http_util "github.com/zitadel/zitadel/internal/api/http" http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
@@ -32,15 +33,18 @@ func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator
return validationCode, nil return validationCode, nil
} }
func NewIAMDomainName(orgName, iamDomain string) string { func NewIAMDomainName(orgName, iamDomain string) (string, error) {
// Reference: label domain requirements https://www.nic.ad.jp/timeline/en/20th/appendix1.html // Reference: label domain requirements https://www.nic.ad.jp/timeline/en/20th/appendix1.html
// Replaces spaces in org name with hyphens // Replaces spaces in org name with hyphens
label := strings.ReplaceAll(orgName, " ", "-") label := strings.ReplaceAll(orgName, " ", "-")
// The label must only contains alphanumeric characters and hyphens // The label must only contains alphanumeric characters and hyphens
// Invalid characters are replaced with and empty space // Invalid characters are replaced with and empty space but as #6471,
label = string(regexp.MustCompile(`[^a-zA-Z0-9-]`).ReplaceAll([]byte(label), []byte(""))) // as these domains are not used to host ZITADEL, but only for user names,
// the characters shouldn't matter that much so we'll accept unicode
// characters, accented characters (\p{L}\p{M}), numbers and hyphens.
label = string(regexp.MustCompile(`[^\p{L}\p{M}0-9-]`).ReplaceAll([]byte(label), []byte("")))
// The label cannot exceed 63 characters // The label cannot exceed 63 characters
if len(label) > 63 { if len(label) > 63 {
@@ -64,7 +68,12 @@ func NewIAMDomainName(orgName, iamDomain string) string {
label = label[:len(label)-1] label = label[:len(label)-1]
} }
return strings.ToLower(label + "." + iamDomain) // Empty string should be invalid
if len(label) > 0 {
return strings.ToLower(label + "." + iamDomain), nil
}
return "", errors.ThrowInvalidArgument(nil, "ORG-RrfXY", "Errors.Org.Domain.EmptyString")
} }
type OrgDomainValidationType int32 type OrgDomainValidationType int32

View File

@@ -41,10 +41,10 @@ func TestNewIAMDomainName(t *testing.T) {
{ {
name: "replace invalid characters [^a-zA-Z0-9-] with empty spaces", name: "replace invalid characters [^a-zA-Z0-9-] with empty spaces",
args: args{ args: args{
orgName: "mí Örg name?", orgName: "mí >**name?",
iamDomain: "localhost", iamDomain: "localhost",
}, },
result: "m-rg-name.localhost", result: "mí-name.localhost",
}, },
{ {
name: "label created from org name size is not greater than 63 chars", name: "label created from org name size is not greater than 63 chars",
@@ -78,10 +78,18 @@ func TestNewIAMDomainName(t *testing.T) {
}, },
result: "my-super-long-organization-name-with-many-many-many-characters.localhost", result: "my-super-long-organization-name-with-many-many-many-characters.localhost",
}, },
{
name: "string full with invalid characters returns empty",
args: args{
orgName: "*¿=@^[])",
iamDomain: "localhost",
},
result: "",
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
domain := NewIAMDomainName(tt.args.orgName, tt.args.iamDomain) domain, _ := NewIAMDomainName(tt.args.orgName, tt.args.iamDomain)
if tt.result != domain { if tt.result != domain {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, domain) t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, domain)
} }

View File

@@ -46,5 +46,6 @@ func (o *Org) GetPrimaryDomain() *OrgDomain {
} }
func (o *Org) AddIAMDomain(iamDomain string) { func (o *Org) AddIAMDomain(iamDomain string) {
o.Domains = append(o.Domains, &OrgDomain{Domain: domain.NewIAMDomainName(o.Name, iamDomain), Verified: true, Primary: true}) orgDomain, _ := domain.NewIAMDomainName(o.Name, iamDomain)
o.Domains = append(o.Domains, &OrgDomain{Domain: orgDomain, Verified: true, Primary: true})
} }

View File

@@ -204,6 +204,7 @@ Errors:
Domain: Domain:
AlreadyExists: Домейнът вече съществува AlreadyExists: Домейнът вече съществува
InvalidCharacter: "Само буквено-цифрови знаци, . " InvalidCharacter: "Само буквено-цифрови знаци, . "
EmptyString: Невалидни нецифрови и азбучни знаци бяха заменени с празни интервали и полученият домейн е празен низ
IDP: IDP:
InvalidSearchQuery: Невалидна заявка за търсене InvalidSearchQuery: Невалидна заявка за търсене
ClientIDMissing: Липсва ClientID ClientIDMissing: Липсва ClientID

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domäne existiert bereits AlreadyExists: Domäne existiert bereits
InvalidCharacter: Nur alphanumerische Zeichen, . und - sind für eine Domäne erlaubt InvalidCharacter: Nur alphanumerische Zeichen, . und - sind für eine Domäne erlaubt
EmptyString: Ungültige nicht numerische und alphabetische Zeichen wurden durch Leerzeichen ersetzt und die resultierende Domäne ist eine leere Zeichenfolge
IDP: IDP:
InvalidSearchQuery: Ungültiger Suchparameter InvalidSearchQuery: Ungültiger Suchparameter
ClientIDMissing: ClientID fehlt ClientIDMissing: ClientID fehlt

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domain already exists AlreadyExists: Domain already exists
InvalidCharacter: Only alphanumeric characters, . and - are allowed for a domain InvalidCharacter: Only alphanumeric characters, . and - are allowed for a domain
EmptyString: Invalid non numeric and alphabetical characters were replaced with empty spaces and resulting domain is an empty string
IDP: IDP:
InvalidSearchQuery: Invalid search query InvalidSearchQuery: Invalid search query
ClientIDMissing: ClientID missing ClientIDMissing: ClientID missing

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: El dominio ya existe AlreadyExists: El dominio ya existe
InvalidCharacter: Solo caracteres alfanuméricos, . y - se permiten para un dominio InvalidCharacter: Solo caracteres alfanuméricos, . y - se permiten para un dominio
EmptyString: Los caracteres alfabéticos y no numéricos no válidos se reemplazaron con espacios vacíos y el dominio resultante es una cadena vacía
IDP: IDP:
InvalidSearchQuery: Consulta de búsqueda no válida InvalidSearchQuery: Consulta de búsqueda no válida
ClientIDMissing: Falta ClientID ClientIDMissing: Falta ClientID

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Le domaine existe déjà AlreadyExists: Le domaine existe déjà
InvalidCharacter: Seuls les caractères alphanumériques, . et - sont autorisés pour un domaine InvalidCharacter: Seuls les caractères alphanumériques, . et - sont autorisés pour un domaine
EmptyString: Les caractères non numériques et alphabétiques non valides ont été remplacés par des espaces vides et le domaine résultant est une chaîne vide
IDP: IDP:
InvalidSearchQuery: Paramètre de recherche non valide InvalidSearchQuery: Paramètre de recherche non valide
ClientIDMissing: ID client manquant ClientIDMissing: ID client manquant

View File

@@ -201,6 +201,8 @@ Errors:
IdpIsNotOIDC: La configurazione IDP non è di tipo oidc IdpIsNotOIDC: La configurazione IDP non è di tipo oidc
Domain: Domain:
AlreadyExists: Il dominio già esistente AlreadyExists: Il dominio già esistente
InvalidCharacter: Solo caratteri alfanumerici, . e - sono consentiti per un dominio
EmptyString: I caratteri non numerici e alfabetici non validi sono stati sostituiti con spazi vuoti e il dominio risultante è una stringa vuota
IDP: IDP:
InvalidSearchQuery: Parametro di ricerca non valido InvalidSearchQuery: Parametro di ricerca non valido
InvalidCharacter: Per un dominio sono ammessi solo caratteri alfanumerici, . e - InvalidCharacter: Per un dominio sono ammessi solo caratteri alfanumerici, . e -

View File

@@ -194,6 +194,7 @@ Errors:
Domain: Domain:
AlreadyExists: ドメインはすでに存在します AlreadyExists: ドメインはすでに存在します
InvalidCharacter: ドメインは英数字、'.'、'-'のみ使用可能です。 InvalidCharacter: ドメインは英数字、'.'、'-'のみ使用可能です。
EmptyString: 無効な数字およびアルファベット以外の文字は空のスペースに置き換えられ、結果のドメインは空の文字列になります
IDP: IDP:
InvalidSearchQuery: 無効な検索クエリです InvalidSearchQuery: 無効な検索クエリです
ClientIDMissing: クライアントIDがありません ClientIDMissing: クライアントIDがありません

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Доменот веќе постои AlreadyExists: Доменот веќе постои
InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен
EmptyString: Неважечките ненумерички и азбучни знаци се заменети со празни места и добиениот домен е празна низа
IDP: IDP:
InvalidSearchQuery: Невалидно пребарување InvalidSearchQuery: Невалидно пребарување
ClientID Missing: ClientID недостасува ClientID Missing: ClientID недостасува

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domena już istnieje AlreadyExists: Domena już istnieje
InvalidCharacter: Tylko znaki alfanumeryczne, . i - są dozwolone dla domeny InvalidCharacter: Tylko znaki alfanumeryczne, . i - są dozwolone dla domeny
EmptyString: Nieprawidłowe znaki inne niż numeryczne i alfabetyczne zostały zastąpione pustymi spacjami, a wynikowa domena jest pustym ciągiem znaków
IDP: IDP:
InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania
ClientIDMissing: Brak ClientID ClientIDMissing: Brak ClientID

View File

@@ -200,6 +200,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domínio já existe AlreadyExists: Domínio já existe
InvalidCharacter: Apenas caracteres alfanuméricos, . e - são permitidos para um domínio InvalidCharacter: Apenas caracteres alfanuméricos, . e - são permitidos para um domínio
EmptyString: Caracteres não numéricos e alfabéticos inválidos foram substituídos por espaços vazios e o domínio resultante é uma string vazia
IDP: IDP:
InvalidSearchQuery: Consulta de pesquisa inválida InvalidSearchQuery: Consulta de pesquisa inválida
ClientIDMissing: ClientID ausente ClientIDMissing: ClientID ausente

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: 域名已存在 AlreadyExists: 域名已存在
InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中 InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中
EmptyString: 无效的非数字和字母字符被替换为空格,结果域是空字符串
IDP: IDP:
InvalidSearchQuery: 无效的搜索查询 InvalidSearchQuery: 无效的搜索查询
ClientIDMissing: 客户端 ID 丢失 ClientIDMissing: 客户端 ID 丢失