feat: restrict languages (#6931)

* feat: return 404 or 409 if org reg disallowed

* fix: system limit permissions

* feat: add iam limits api

* feat: disallow public org registrations on default instance

* add integration test

* test: integration

* fix test

* docs: describe public org registrations

* avoid updating docs deps

* fix system limits integration test

* silence integration tests

* fix linting

* ignore strange linter complaints

* review

* improve reset properties naming

* redefine the api

* use restrictions aggregate

* test query

* simplify and test projection

* test commands

* fix unit tests

* move integration test

* support restrictions on default instance

* also test GetRestrictions

* self review

* lint

* abstract away resource owner

* fix tests

* configure supported languages

* fix allowed languages

* fix tests

* default lang must not be restricted

* preferred language must be allowed

* change preferred languages

* check languages everywhere

* lint

* test command side

* lint

* add integration test

* add integration test

* restrict supported ui locales

* lint

* lint

* cleanup

* lint

* allow undefined preferred language

* fix integration tests

* update main

* fix env var

* ignore linter

* ignore linter

* improve integration test config

* reduce cognitive complexity

* compile

* check for duplicates

* remove useless restriction checks

* review

* revert restriction renaming

* fix language restrictions

* lint

* generate

* allow custom texts for supported langs for now

* fix tests

* cleanup

* cleanup

* cleanup

* lint

* unsupported preferred lang is allowed

* fix integration test

* finish reverting to old property name

* finish reverting to old property name

* load languages

* refactor(i18n): centralize translators and fs

* lint

* amplify no validations on preferred languages

* fix integration test

* lint

* fix resetting allowed languages

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

View File

@@ -13,6 +13,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/repository/feature"
@@ -107,18 +108,22 @@ type InstanceSetup struct {
EmailTemplate []byte
MessageTexts []*domain.CustomMessageText
SMTPConfiguration *smtp.Config
OIDCSettings *struct {
AccessTokenLifetime time.Duration
IdTokenLifetime time.Duration
RefreshTokenIdleExpiration time.Duration
RefreshTokenExpiration time.Duration
}
Quotas *struct {
Items []*SetQuota
}
Features map[domain.Feature]any
Limits *SetLimits
Restrictions *SetRestrictions
OIDCSettings *OIDCSettings
Quotas *SetQuotas
Features map[domain.Feature]any
Limits *SetLimits
Restrictions *SetRestrictions
}
type OIDCSettings struct {
AccessTokenLifetime time.Duration
IdTokenLifetime time.Duration
RefreshTokenIdleExpiration time.Duration
RefreshTokenExpiration time.Duration
}
type SetQuotas struct {
Items []*SetQuota
}
type SecretGenerators struct {
@@ -289,183 +294,32 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate),
}
if setup.Quotas != nil {
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))
}
if err := setupQuotas(c, &validations, setup.Quotas, instanceID); err != nil {
return "", "", nil, nil, err
}
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,
}
setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
validations = append(validations,
AddOrgCommand(ctx, orgAgg, setup.Org.Name),
c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID),
)
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)
pat, machineKey, err := setupAdmin(c, &validations, setup.Org.Machine, setup.Org.Human, orgID, userID, userAgg)
if err != nil {
return "", "", nil, nil, err
}
validations = append(validations, addGeneratedDomain...)
if setup.CustomDomain != "" {
validations = append(validations,
c.addInstanceDomain(instanceAgg, setup.CustomDomain, false),
setPrimaryInstanceDomain(instanceAgg, setup.CustomDomain),
)
setupMinimalInterfaces(c, &validations, instanceAgg, projectAgg, orgAgg, userID, setup.zitadel)
if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil {
return "", "", nil, nil, err
}
if setup.SMTPConfiguration != nil {
validations = append(validations,
c.prepareAddSMTPConfig(
instanceAgg,
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))
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
if err := setupFeatures(c, &validations, setup.Features, instanceID); err != nil {
return "", "", nil, nil, err
}
setupLimits(c, &validations, limitsAgg, setup.Limits)
setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions)
//nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
if err != nil {
return "", "", nil, nil, err
@@ -488,6 +342,205 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
}, 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) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
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 {
return func() (preparation.CreateCommands, error) {
if defaultLanguage == language.Und {
return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument")
if err := domain.LanguageIsDefined(defaultLanguage); err != nil {
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) {
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 {
return nil, err
}
if writeModel.DefaultLanguage == defaultLanguage {
return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged")
if err := domain.LanguageIsAllowed(false, restrictionsWM.allowedLanguages, defaultLanguage); err != nil {
return nil, err
}
if err != nil {
return nil, err
}
return []eventstore.Command{instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage)}, nil
}, nil