diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 2d1d171b9..891828941 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -761,6 +761,8 @@ DefaultInstance: Greeting: Hello {{.DisplayName}}, Text: The password of your user has changed. If this change was not done by you, please be advised to immediately reset your password. ButtonText: Login + Features: + - FeatureLoginDefaultOrg: true Quotas: # Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist. @@ -819,6 +821,7 @@ InternalAuthZ: - "iam.flow.read" - "iam.flow.write" - "iam.flow.delete" + - "iam.feature.write" - "org.read" - "org.global.read" - "org.create" diff --git a/cmd/ready/config.go b/cmd/ready/config.go index 2b7b56ac0..b9137519e 100644 --- a/cmd/ready/config.go +++ b/cmd/ready/config.go @@ -23,6 +23,7 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), + hook.StringToFeatureHookFunc(), )), ) logging.OnError(err).Fatal("unable to read default config") diff --git a/cmd/setup/03.go b/cmd/setup/03.go index ffa4769a4..4de513be4 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -24,6 +24,7 @@ type FirstInstance struct { Org command.InstanceOrgSetup MachineKeyPath string PatPath string + Features map[domain.Feature]any instanceSetup command.InstanceSetup userEncryptionKey *crypto.KeyConfig diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 3290b59e6..068f524b9 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -43,6 +43,7 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), database.DecodeHook, + hook.StringToFeatureHookFunc(), )), ) logging.OnError(err).Fatal("unable to read default config") @@ -99,6 +100,7 @@ func MustNewSteps(v *viper.Viper) *Steps { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), + hook.StringToFeatureHookFunc(), )), ) logging.OnError(err).Fatal("unable to read steps") diff --git a/cmd/start/config.go b/cmd/start/config.go index 6280fdea4..6ef844fee 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -92,6 +92,7 @@ func MustNewConfig(v *viper.Viper) *Config { database.DecodeHook, actions.HTTPConfigDecodeHook, systemAPIUsersDecodeHook, + hook.StringToFeatureHookFunc(), )), ) logging.OnError(err).Fatal("unable to read config") diff --git a/cmd/start/start.go b/cmd/start/start.go index b7eb8e59c..2ee0725b6 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -27,6 +27,7 @@ import ( "github.com/zitadel/zitadel/cmd/build" "github.com/zitadel/zitadel/cmd/key" cmd_tls "github.com/zitadel/zitadel/cmd/tls" + "github.com/zitadel/zitadel/feature" "github.com/zitadel/zitadel/internal/actions" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" "github.com/zitadel/zitadel/internal/api" @@ -412,7 +413,27 @@ func startAPIs( } apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c) - l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.WithoutLimiting().Handle, keys.User, keys.IDPConfig, keys.CSRFCookieKey) + l, err := login.CreateLogin( + config.Login, + commands, + queries, + authRepo, + store, + console.HandlerPrefix+"/", + op.AuthCallbackURL(oidcProvider), + provider.AuthCallbackURL(samlProvider), + config.ExternalSecure, + userAgentInterceptor, + op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, + provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, + instanceInterceptor.Handler, + assetsCache.Handler, + limitingAccessInterceptor.WithoutLimiting().Handle, + keys.User, + keys.IDPConfig, + keys.CSRFCookieKey, + feature.NewCheck(eventstore), + ) if err != nil { return fmt.Errorf("unable to start login: %w", err) } diff --git a/feature/check.go b/feature/check.go new file mode 100644 index 000000000..eb66ce119 --- /dev/null +++ b/feature/check.go @@ -0,0 +1,45 @@ +package feature + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/feature" +) + +type Checker interface { + CheckInstanceBooleanFeature(ctx context.Context, f domain.Feature) (feature.Boolean, error) +} + +func NewCheck(eventstore *eventstore.Eventstore) *Check { + return &Check{eventstore: eventstore} +} + +type Check struct { + eventstore *eventstore.Eventstore +} + +func (c *Check) CheckInstanceBooleanFeature(ctx context.Context, f domain.Feature) (feature.Boolean, error) { + return getInstanceFeature[feature.Boolean](ctx, f, c.eventstore.Filter) +} + +func getInstanceFeature[T feature.SetEventType](ctx context.Context, f domain.Feature, filter preparation.FilterToQueryReducer) (T, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + writeModel, err := command.NewInstanceFeatureWriteModel[T](instanceID, f) + if err != nil { + return writeModel.Value, err + } + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return writeModel.Value, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return writeModel.Value, err + } + return writeModel.Value, nil +} diff --git a/go.mod b/go.mod index 9ea9150a1..a50b0341c 100644 --- a/go.mod +++ b/go.mod @@ -87,6 +87,7 @@ require ( require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect + github.com/dmarkham/enumer v1.5.8 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect @@ -101,9 +102,12 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect + github.com/pascaldekloe/name v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/smartystreets/assertions v1.0.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect ) diff --git a/go.sum b/go.sum index 45d253f35..afff809df 100644 --- a/go.sum +++ b/go.sum @@ -179,6 +179,8 @@ github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwu github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= +github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog= @@ -709,6 +711,8 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= +github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= @@ -989,6 +993,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1222,6 +1228,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= +golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/api/grpc/admin/feature.go b/internal/api/grpc/admin/feature.go new file mode 100644 index 000000000..2fe443d5e --- /dev/null +++ b/internal/api/grpc/admin/feature.go @@ -0,0 +1,20 @@ +package admin + +import ( + "context" + + object_pb "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/domain" + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" +) + +func (s *Server) ActivateFeatureLoginDefaultOrg(ctx context.Context, _ *admin_pb.ActivateFeatureLoginDefaultOrgRequest) (*admin_pb.ActivateFeatureLoginDefaultOrgResponse, error) { + details, err := s.command.SetBooleanInstanceFeature(ctx, domain.FeatureLoginDefaultOrg, true) + if err != nil { + return nil, err + } + return &admin_pb.ActivateFeatureLoginDefaultOrgResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil + +} diff --git a/internal/api/grpc/system/feature.go b/internal/api/grpc/system/feature.go new file mode 100644 index 000000000..a577af4f0 --- /dev/null +++ b/internal/api/grpc/system/feature.go @@ -0,0 +1,34 @@ +package system + +import ( + "context" + + object_pb "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + system_pb "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func (s *Server) SetInstanceFeature(ctx context.Context, req *system_pb.SetInstanceFeatureRequest) (*system_pb.SetInstanceFeatureResponse, error) { + details, err := s.setInstanceFeature(ctx, req) + if err != nil { + return nil, err + } + return &system_pb.SetInstanceFeatureResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil + +} + +func (s *Server) setInstanceFeature(ctx context.Context, req *system_pb.SetInstanceFeatureRequest) (*domain.ObjectDetails, error) { + feat := domain.Feature(req.FeatureId) + if !feat.IsAFeature() { + return nil, errors.ThrowInvalidArgument(nil, "SYST-SGV45", "Errors.Feature.NotExisting") + } + switch t := req.Value.(type) { + case *system_pb.SetInstanceFeatureRequest_Bool: + return s.command.SetBooleanInstanceFeature(ctx, feat, t.Bool) + default: + return nil, errors.ThrowInvalidArgument(nil, "SYST-dag5g", "Errors.Feature.TypeNotSupported") + } +} diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index c98fbbff4..4c5bd0d60 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/mux" "github.com/rakyll/statik/fs" + "github.com/zitadel/zitadel/feature" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -40,6 +41,7 @@ type Login struct { samlAuthCallbackURL func(context.Context, string) string idpConfigAlg crypto.EncryptionAlgorithm userCodeAlg crypto.EncryptionAlgorithm + featureCheck feature.Checker } type Config struct { @@ -76,6 +78,7 @@ func CreateLogin(config Config, userCodeAlg crypto.EncryptionAlgorithm, idpConfigAlg crypto.EncryptionAlgorithm, csrfCookieKey []byte, + featureCheck feature.Checker, ) (*Login, error) { login := &Login{ oidcAuthCallbackURL: oidcAuthCallbackURL, @@ -88,6 +91,7 @@ func CreateLogin(config Config, authRepo: authRepo, idpConfigAlg: idpConfigAlg, userCodeAlg: userCodeAlg, + featureCheck: featureCheck, } statikFS, err := fs.NewWithNamespace("login") if err != nil { diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index 29fed4eda..85d54cd01 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -506,25 +506,19 @@ func (l *Login) getOrgID(r *http.Request, authReq *domain.AuthRequest) string { } func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string { - privateLabelingOrgID := authz.GetInstance(r.Context()).InstanceID() - if authReq == nil { - if id := r.FormValue(queryOrgID); id != "" { - return id - } - return privateLabelingOrgID + defaultID := authz.GetInstance(r.Context()).DefaultOrganisationID() + f, err := l.featureCheck.CheckInstanceBooleanFeature(r.Context(), domain.FeatureLoginDefaultOrg) + logging.OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg) + if !f.Boolean { + defaultID = authz.GetInstance(r.Context()).InstanceID() } - if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified { - privateLabelingOrgID = authReq.ApplicationResourceOwner + if authReq != nil { + return authReq.PrivateLabelingOrgID(defaultID) } - if authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingUnspecified { - if authReq.UserOrgID != "" { - privateLabelingOrgID = authReq.UserOrgID - } + if id := r.FormValue(queryOrgID); id != "" { + return id } - if authReq.RequestedOrgID != "" { - privateLabelingOrgID = authReq.RequestedOrgID - } - return privateLabelingOrgID + return defaultID } func (l *Login) getOrgName(authReq *domain.AuthRequest) string { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 950809f0c..dde603495 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/logging" + "github.com/zitadel/zitadel/feature" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view" cache "github.com/zitadel/zitadel/internal/auth_request/repository" @@ -52,6 +53,8 @@ type AuthRequestRepo struct { ApplicationProvider applicationProvider CustomTextProvider customTextProvider + FeatureCheck feature.Checker + IdGenerator id.Generator } @@ -650,7 +653,12 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A orgID = request.UserOrgID } if orgID == "" { - orgID = authz.GetInstance(ctx).InstanceID() + orgID = authz.GetInstance(ctx).DefaultOrganisationID() + f, err := repo.FeatureCheck.CheckInstanceBooleanFeature(ctx, domain.FeatureLoginDefaultOrg) + logging.WithFields("authReq", request.ID).OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg) + if !f.Boolean { + orgID = authz.GetInstance(ctx).InstanceID() + } } loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, orgID) @@ -671,19 +679,7 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A return err } request.PrivacyPolicy = privacyPolicy - privateLabelingOrgID := authz.GetInstance(ctx).InstanceID() - if request.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified { - privateLabelingOrgID = request.ApplicationResourceOwner - } - if request.PrivateLabelingSetting == domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || request.PrivateLabelingSetting == domain.PrivateLabelingSettingUnspecified { - if request.UserOrgID != "" { - privateLabelingOrgID = request.UserOrgID - } - } - if request.RequestedOrgID != "" { - privateLabelingOrgID = request.RequestedOrgID - } - labelPolicy, err := repo.getLabelPolicy(ctx, privateLabelingOrgID) + labelPolicy, err := repo.getLabelPolicy(ctx, request.PrivateLabelingOrgID(orgID)) if err != nil { return err } @@ -749,8 +745,9 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain } // the user was either not found or not active // so check if the loginname suffix matches a verified org domain - if repo.checkDomainDiscovery(ctx, request, loginName) { - return nil + ok, err := repo.checkDomainDiscovery(ctx, request, loginName) + if err != nil || ok { + return err } // let's once again check if the user was just inactive if user != nil && user.State == int32(domain.UserStateInactive) { @@ -782,30 +779,34 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain return errors.ThrowInternal(nil, "AUTH-asf3df", "Errors.Internal") } -func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request *domain.AuthRequest, loginName string) bool { +func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request *domain.AuthRequest, loginName string) (bool, error) { // check if there's a suffix in the loginname loginName = strings.TrimSpace(strings.ToLower(loginName)) index := strings.LastIndex(loginName, "@") if index < 0 { - return false + return false, nil } // check if the suffix matches a verified domain org, err := repo.Query.OrgByVerifiedDomain(ctx, loginName[index+1:]) if err != nil { - return false + return false, nil } // and if the login policy allows domain discovery policy, err := repo.Query.LoginPolicyByID(ctx, true, org.ID, false) if err != nil || !policy.AllowDomainDiscovery { - return false + return false, nil } // discovery was allowed, so set the org as requested org // and clear all potentially existing user information and only set the loginname as hint (for registration) + // also ensure that the policies are read from the org request.SetOrgInformation(org.ID, org.Name, org.Domain, false) request.SetUserInfo("", "", "", "", "", org.ID) + if err = repo.fillPolicies(ctx, request); err != nil { + return false, err + } request.LoginHint = loginName request.Prompt = append(request.Prompt, domain.PromptCreate) // to trigger registration - return true + return true, nil } func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) { diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 257b49d70..b8d41e009 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -3,6 +3,7 @@ package eventsourcing import ( "context" + "github.com/zitadel/zitadel/feature" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/eventstore" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/spooler" auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view" @@ -88,6 +89,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c ProjectProvider: queryView, ApplicationProvider: queries, CustomTextProvider: queries, + FeatureCheck: feature.NewCheck(esV2), IdGenerator: idGenerator, }, eventstore.TokenRepo{ diff --git a/internal/command/command.go b/internal/command/command.go index aa8c1f3e7..9e8e78e39 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -16,6 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/repository/action" "github.com/zitadel/zitadel/internal/repository/authrequest" + "github.com/zitadel/zitadel/internal/repository/feature" "github.com/zitadel/zitadel/internal/repository/idpintent" instance_repo "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/keypair" @@ -145,6 +146,7 @@ func StartCommands( authrequest.RegisterEventMappers(repo.eventstore) oidcsession.RegisterEventMappers(repo.eventstore) milestone.RegisterEventMappers(repo.eventstore) + feature.RegisterEventMappers(repo.eventstore) repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost) repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher() diff --git a/internal/command/instance.go b/internal/command/instance.go index 1677d930f..a68645237 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -15,6 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/repository/feature" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" @@ -112,6 +113,7 @@ type InstanceSetup struct { Quotas *struct { Items []*SetQuota } + Features map[domain.Feature]any } type SecretGenerators struct { @@ -432,6 +434,19 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str ) } + 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") + } + } + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { return "", "", nil, nil, err diff --git a/internal/command/instance_feature.go b/internal/command/instance_feature.go new file mode 100644 index 000000000..6aee52c09 --- /dev/null +++ b/internal/command/instance_feature.go @@ -0,0 +1,63 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/repository/feature" +) + +func (c *Commands) SetBooleanInstanceFeature(ctx context.Context, f domain.Feature, value bool) (*domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + writeModel, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f) + if err != nil { + return nil, err + } + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, + prepareSetFeature(writeModel, feature.Boolean{Boolean: value}, c.idGenerator)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + return writeModelToObjectDetails(&writeModel.FeatureWriteModel.WriteModel), nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func prepareSetFeature[T feature.SetEventType](writeModel *InstanceFeatureWriteModel[T], value T, idGenerator id.Generator) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if !writeModel.feature.IsAFeature() || writeModel.feature == domain.FeatureUnspecified { + return nil, errors.ThrowPreconditionFailed(nil, "FEAT-JK3td", "Errors.Feature.NotExisting") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if len(events) == 0 { + writeModel.AggregateID, err = idGenerator.Next() + if err != nil { + return nil, err + } + } + setEvent, err := writeModel.Set(ctx, value) + if err != nil || setEvent == nil { + return nil, err + } + return []eventstore.Command{setEvent}, nil + }, nil + } +} diff --git a/internal/command/instance_feature_model.go b/internal/command/instance_feature_model.go new file mode 100644 index 000000000..1389735ca --- /dev/null +++ b/internal/command/instance_feature_model.go @@ -0,0 +1,81 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/feature" +) + +type FeatureWriteModel[T feature.SetEventType] struct { + eventstore.WriteModel + + feature domain.Feature + + Value T +} + +func NewFeatureWriteModel[T feature.SetEventType](instanceID, resourceOwner string, feature domain.Feature) (*FeatureWriteModel[T], error) { + wm := &FeatureWriteModel[T]{ + WriteModel: eventstore.WriteModel{ + InstanceID: instanceID, + ResourceOwner: resourceOwner, + }, + feature: feature, + } + if wm.Value.FeatureType() != feature.Type() { + return nil, errors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue") + } + return wm, nil +} +func (wm *FeatureWriteModel[T]) Set(ctx context.Context, value T) (event *feature.SetEvent[T], err error) { + if wm.Value == value { + return nil, nil + } + return feature.NewSetEvent[T]( + ctx, + &feature.NewAggregate(wm.AggregateID, wm.ResourceOwner).Aggregate, + wm.eventType(), + value, + ), nil +} + +func (wm *FeatureWriteModel[T]) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(feature.AggregateType). + EventTypes(wm.eventType()). + Builder() +} + +func (wm *FeatureWriteModel[T]) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *feature.SetEvent[T]: + wm.Value = e.Value + default: + return errors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported") + } + } + return wm.WriteModel.Reduce() +} + +func (wm *FeatureWriteModel[T]) eventType() eventstore.EventType { + return feature.EventTypeFromFeature(wm.feature) +} + +type InstanceFeatureWriteModel[T feature.SetEventType] struct { + FeatureWriteModel[T] +} + +func NewInstanceFeatureWriteModel[T feature.SetEventType](instanceID string, feature domain.Feature) (*InstanceFeatureWriteModel[T], error) { + wm, err := NewFeatureWriteModel[T](instanceID, instanceID, feature) + if err != nil { + return nil, err + } + return &InstanceFeatureWriteModel[T]{ + FeatureWriteModel: *wm, + }, nil +} diff --git a/internal/command/instance_feature_test.go b/internal/command/instance_feature_test.go new file mode 100644 index 000000000..bf07ec2c6 --- /dev/null +++ b/internal/command/instance_feature_test.go @@ -0,0 +1,179 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/feature" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +func TestCommands_SetBooleanInstanceFeature(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + ctx context.Context + f domain.Feature + value bool + } + type res struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "unknown feature", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + f: domain.FeatureUnspecified, + value: true, + }, + res{ + err: errors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue"), + }, + }, + { + "wrong type", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithInstanceID("instanceID", + // as there's currently no other [feature.SetEventType] than [feature.Boolean], + // we need to use a completely other event type to demonstrate the behaviour + instance.NewInstanceAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate, + "instance", + ), + ), + ), + ), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + f: domain.FeatureLoginDefaultOrg, + value: true, + }, + res{ + err: errors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported"), + }, + }, + { + "first set", + fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID("instanceID", + feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate, + feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg), + feature.Boolean{Boolean: true}, + ), + ), + }, + ), + ), + idGenerator: mock.ExpectID(t, "featureID"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + f: domain.FeatureLoginDefaultOrg, + value: true, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instanceID", + }, + }, + }, + { + "update flag", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithInstanceID("instanceID", + feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate, + feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg), + feature.Boolean{Boolean: true}, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID("instanceID", + feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate, + feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg), + feature.Boolean{Boolean: false}, + ), + ), + }, + ), + ), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + f: domain.FeatureLoginDefaultOrg, + value: false, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instanceID", + }, + }, + }, + { + "no change", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithInstanceID("instanceID", + feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate, + feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg), + feature.Boolean{Boolean: true}, + ), + ), + ), + ), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + f: domain.FeatureLoginDefaultOrg, + value: true, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instanceID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + } + got, err := c.SetBooleanInstanceFeature(tt.args.ctx, tt.args.f, tt.args.value) + assert.ErrorIs(t, err, tt.res.err) + assert.Equal(t, tt.res.details, got) + }) + } +} diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 2d798a40e..ba886e5c4 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -20,6 +20,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/repository/mock" action_repo "github.com/zitadel/zitadel/internal/repository/action" "github.com/zitadel/zitadel/internal/repository/authrequest" + "github.com/zitadel/zitadel/internal/repository/feature" "github.com/zitadel/zitadel/internal/repository/idpintent" iam_repo "github.com/zitadel/zitadel/internal/repository/instance" key_repo "github.com/zitadel/zitadel/internal/repository/keypair" @@ -52,6 +53,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore { authrequest.RegisterEventMappers(es) oidcsession.RegisterEventMappers(es) quota_repo.RegisterEventMappers(es) + feature.RegisterEventMappers(es) return es } diff --git a/internal/config/hook/feature.go b/internal/config/hook/feature.go new file mode 100644 index 000000000..5eccaa570 --- /dev/null +++ b/internal/config/hook/feature.go @@ -0,0 +1,27 @@ +package hook + +import ( + "reflect" + + "github.com/mitchellh/mapstructure" + + "github.com/zitadel/zitadel/internal/domain" +) + +func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType { + return func( + f reflect.Type, + t reflect.Type, + data interface{}, + ) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + + if t != reflect.TypeOf(domain.FeatureUnspecified) { + return data, nil + } + + return domain.FeatureString(data.(string)) + } +} diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index d5b1bf4fe..acd9e95ed 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -208,3 +208,17 @@ func (a *AuthRequest) Done() bool { } return false } + +func (a *AuthRequest) PrivateLabelingOrgID(defaultID string) string { + if a.RequestedOrgID != "" { + return a.RequestedOrgID + } + if (a.PrivateLabelingSetting == PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || a.PrivateLabelingSetting == PrivateLabelingSettingUnspecified) && + a.UserOrgID != "" { + return a.UserOrgID + } + if a.PrivateLabelingSetting != PrivateLabelingSettingUnspecified { + return a.ApplicationResourceOwner + } + return defaultID +} diff --git a/internal/domain/feature.go b/internal/domain/feature.go new file mode 100644 index 000000000..47f58c991 --- /dev/null +++ b/internal/domain/feature.go @@ -0,0 +1,28 @@ +//go:generate enumer -type Feature + +package domain + +type Feature int + +func (f Feature) Type() FeatureType { + switch f { + case FeatureUnspecified: + return FeatureTypeUnspecified + case FeatureLoginDefaultOrg: + return FeatureTypeBoolean + default: + return FeatureTypeUnspecified + } +} + +const ( + FeatureTypeUnspecified FeatureType = iota + FeatureTypeBoolean +) + +type FeatureType int + +const ( + FeatureUnspecified Feature = iota + FeatureLoginDefaultOrg +) diff --git a/internal/domain/feature_enumer.go b/internal/domain/feature_enumer.go new file mode 100644 index 000000000..008ec8720 --- /dev/null +++ b/internal/domain/feature_enumer.go @@ -0,0 +1,78 @@ +// Code generated by "enumer -type Feature"; DO NOT EDIT. + +package domain + +import ( + "fmt" + "strings" +) + +const _FeatureName = "FeatureUnspecifiedFeatureLoginDefaultOrg" + +var _FeatureIndex = [...]uint8{0, 18, 40} + +const _FeatureLowerName = "featureunspecifiedfeaturelogindefaultorg" + +func (i Feature) String() string { + if i < 0 || i >= Feature(len(_FeatureIndex)-1) { + return fmt.Sprintf("Feature(%d)", i) + } + return _FeatureName[_FeatureIndex[i]:_FeatureIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _FeatureNoOp() { + var x [1]struct{} + _ = x[FeatureUnspecified-(0)] + _ = x[FeatureLoginDefaultOrg-(1)] +} + +var _FeatureValues = []Feature{FeatureUnspecified, FeatureLoginDefaultOrg} + +var _FeatureNameToValueMap = map[string]Feature{ + _FeatureName[0:18]: FeatureUnspecified, + _FeatureLowerName[0:18]: FeatureUnspecified, + _FeatureName[18:40]: FeatureLoginDefaultOrg, + _FeatureLowerName[18:40]: FeatureLoginDefaultOrg, +} + +var _FeatureNames = []string{ + _FeatureName[0:18], + _FeatureName[18:40], +} + +// FeatureString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func FeatureString(s string) (Feature, error) { + if val, ok := _FeatureNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _FeatureNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Feature values", s) +} + +// FeatureValues returns all values of the enum +func FeatureValues() []Feature { + return _FeatureValues +} + +// FeatureStrings returns a slice of all String values of the enum +func FeatureStrings() []string { + strs := make([]string, len(_FeatureNames)) + copy(strs, _FeatureNames) + return strs +} + +// IsAFeature returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Feature) IsAFeature() bool { + for _, v := range _FeatureValues { + if i == v { + return true + } + } + return false +} diff --git a/internal/repository/feature/aggregate.go b/internal/repository/feature/aggregate.go new file mode 100644 index 000000000..479df776d --- /dev/null +++ b/internal/repository/feature/aggregate.go @@ -0,0 +1,30 @@ +package feature + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + eventTypePrefix = eventstore.EventType("feature.") + setSuffix = ".set" +) + +const ( + AggregateType = "feature" + AggregateVersion = "v1" +) + +type Aggregate struct { + eventstore.Aggregate +} + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/feature/eventstore.go b/internal/repository/feature/eventstore.go new file mode 100644 index 000000000..4fe6a3727 --- /dev/null +++ b/internal/repository/feature/eventstore.go @@ -0,0 +1,9 @@ +package feature + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +func RegisterEventMappers(es *eventstore.Eventstore) { + es.RegisterFilterEventMapper(AggregateType, DefaultLoginInstanceEventType, eventstore.GenericEventMapper[SetEvent[Boolean]]) +} diff --git a/internal/repository/feature/feature.go b/internal/repository/feature/feature.go new file mode 100644 index 000000000..b3757c103 --- /dev/null +++ b/internal/repository/feature/feature.go @@ -0,0 +1,65 @@ +package feature + +import ( + "context" + "strings" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + DefaultLoginInstanceEventType = EventTypeFromFeature(domain.FeatureLoginDefaultOrg) +) + +func EventTypeFromFeature(feature domain.Feature) eventstore.EventType { + return eventTypePrefix + eventstore.EventType(strings.ToLower(feature.String())) + setSuffix +} + +type SetEvent[T SetEventType] struct { + *eventstore.BaseEvent + + Value T +} + +func (e *SetEvent[T]) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *SetEvent[T]) Data() interface{} { + return e +} + +func (e *SetEvent[T]) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +type SetEventType interface { + Boolean + FeatureType() domain.FeatureType +} + +type EventType[T SetEventType] struct { + eventstore.EventType +} + +type Boolean struct { + Boolean bool +} + +func (b Boolean) FeatureType() domain.FeatureType { + return domain.FeatureTypeBoolean +} + +func NewSetEvent[T SetEventType]( + ctx context.Context, + aggregate *eventstore.Aggregate, + eventType eventstore.EventType, + setType T, +) *SetEvent[T] { + return &SetEvent[T]{ + eventstore.NewBaseEventForPush( + ctx, aggregate, eventType), + setType, + } +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 4a8d2e762..d1540b262 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -522,6 +522,10 @@ Errors: Token: Invalid: Токенът е невалиден Expired: Токенът е изтекъл + Feature: + NotExisting: Функцията не съществува + TypeNotSupported: Типът функция не се поддържа + InvalidValue: Невалидна стойност за тази функция AggregateTypes: action: Действие @@ -532,6 +536,8 @@ AggregateTypes: user: Потребител usergrant: Предоставяне на потребител quota: Квота + feature: Особеност + EventTypes: user: added: Добавен потребител diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index fe31dee4a..ca7d9d45a 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -505,6 +505,10 @@ Errors: Invalid: Token ist ungültig Expired: Token ist abgelaufen InvalidClient: Token wurde nicht für diesen Client ausgestellt + Feature: + NotExisting: Feature existiert nicht + TypeNotSupported: Feature Typ wird nicht unterstützt + InvalidValue: Ungültiger Wert für dieses Feature AggregateTypes: action: Action @@ -515,6 +519,7 @@ AggregateTypes: user: Benutzer usergrant: Benutzerberechtigung quota: Kontingent + feature: Feature EventTypes: user: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 3c74e9046..82df7a593 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -505,6 +505,10 @@ Errors: Invalid: Token is invalid Expired: Token is expired InvalidClient: Token was not issued for this client + Feature: + NotExisting: Feature does not exist + TypeNotSupported: Feature type is not supported + InvalidValue: Invalid value for this feature AggregateTypes: action: Action @@ -515,6 +519,7 @@ AggregateTypes: user: User usergrant: User grant quota: Quota + feature: Feature EventTypes: user: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 486327606..f45ecdcff 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -505,6 +505,10 @@ Errors: Invalid: El token no es válido Expired: El token ha caducado InvalidClient: El token no ha sido emitido para este cliente + Feature: + NotExisting: La característica no existe + TypeNotSupported: El tipo de característica no es compatible + InvalidValue: Valor no válido para esta característica AggregateTypes: action: Acción @@ -515,6 +519,7 @@ AggregateTypes: user: Usuario usergrant: Concesión de usuario quota: Cuota + feature: Característica EventTypes: user: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index e0259d0a9..150cb17c0 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -505,6 +505,10 @@ Errors: Invalid: Le jeton n'est pas valide Expired: Le jeton est expiré InvalidClient: Le token n'a pas été émis pour ce client + Feature: + NotExisting: La fonctionnalité n'existe pas + TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge + InvalidValue: Valeur non valide pour cette fonctionnalité AggregateTypes: action: Action @@ -515,6 +519,7 @@ AggregateTypes: user: Utilisateur usergrant: Subvention de l'utilisateur quota: Contingent + feature: Fonctionnalité EventTypes: user: diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index b175d2f35..bd38e9a47 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -505,6 +505,10 @@ Errors: Invalid: Token non è valido Expired: Token è scaduto InvalidClient: Il token non è stato emesso per questo cliente + Feature: + NotExisting: La funzionalità non esiste + TypeNotSupported: Il tipo di funzionalità non è supportato + InvalidValue: Valore non valido per questa funzionalità AggregateTypes: action: Azione @@ -515,6 +519,7 @@ AggregateTypes: user: Utente usergrant: Sovvenzione utente quota: Quota + feature: Funzionalità EventTypes: user: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index b5178ee50..cfd1625fc 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -494,6 +494,10 @@ Errors: Invalid: トークンが無効です Expired: トークンの有効期限が切れている InvalidClient: トークンが発行されていません + Feature: + NotExisting: 機能が存在しません + TypeNotSupported: 機能タイプはサポートされていません + InvalidValue: この機能には無効な値です AggregateTypes: action: アクション @@ -504,6 +508,7 @@ AggregateTypes: user: ユーザー usergrant: ユーザーグラント quota: クォータ + feature: 特徴 EventTypes: user: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index e458b45d0..026da0aeb 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -505,6 +505,10 @@ Errors: Invalid: токенот е неважечки Expired: токенот е истечен InvalidClient: Токен не беше издаден на овој клиент + Feature: + NotExisting: Функцијата не постои + TypeNotSupported: Типот на функција не е поддржан + InvalidValue: Неважечка вредност за оваа функција AggregateTypes: action: Акција @@ -515,6 +519,7 @@ AggregateTypes: user: Корисник usergrant: Овластување на корисник quota: Квота + feature: Карактеристика EventTypes: user: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index a3f283cfe..bd3fb70e9 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -505,6 +505,10 @@ Errors: Invalid: Token jest nieprawidłowy Expired: Token wygasł InvalidClient: Token nie został wydany dla tego klienta + Feature: + NotExisting: Funkcja nie istnieje + TypeNotSupported: Typ funkcji nie jest obsługiwany + InvalidValue: Nieprawidłowa wartość dla tej funkcji AggregateTypes: action: Działanie @@ -515,6 +519,7 @@ AggregateTypes: user: Użytkownik usergrant: Uprawnienie użytkownika quota: Limit + feature: Funkcja EventTypes: user: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 753ec1a79..eab7ff98e 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -499,6 +499,10 @@ Errors: WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login OIDCSession: RefreshTokenInvalid: O Refresh Token é inválido + Feature: + NotExisting: O recurso não existe + TypeNotSupported: O tipo de recurso não é compatível + InvalidValue: Valor inválido para este recurso AggregateTypes: action: Ação @@ -509,6 +513,7 @@ AggregateTypes: user: Usuário usergrant: Concessão de usuário quota: Cota + feature: Recurso EventTypes: user: diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index bf7d1475b..fe381c029 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -505,6 +505,10 @@ Errors: Invalid: 令牌无效 Expired: 令牌已过期 InvalidClient: 没有为该客户发放令牌 + Feature: + NotExisting: 功能不存在 + TypeNotSupported: 不支持功能类型 + InvalidValue: 此功能的值无效 AggregateTypes: action: 动作 @@ -515,6 +519,7 @@ AggregateTypes: user: 用户 usergrant: 用户授权 quota: 配额 + feature: 特征 EventTypes: user: diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 8748d1130..e20e52e42 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -3706,6 +3706,19 @@ service AdminService { description: "Returns a list of the possible aggregate types in ZITADEL. This is used to filter the aggregate types in the list events request." }; } + + // Activates the "LoginDefaultOrg" feature by setting the flag to "true" + // This is irreversible! + // Once activated, the login UI will use the settings of the default org (and not from the instance) if not organisation context is set + rpc ActivateFeatureLoginDefaultOrg(ActivateFeatureLoginDefaultOrgRequest) returns (ActivateFeatureLoginDefaultOrgResponse) { + option (google.api.http) = { + put: "/features/login_default_org" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.feature.write"; + }; + } } @@ -7740,3 +7753,9 @@ message ListAggregateTypesRequest {} message ListAggregateTypesResponse { repeated zitadel.event.v1.AggregateType aggregate_types = 1; } + +message ActivateFeatureLoginDefaultOrgRequest {} + +message ActivateFeatureLoginDefaultOrgResponse { + zitadel.v1.ObjectDetails details = 1; +} \ No newline at end of file diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 0194dbb23..ad311b2de 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -397,6 +397,18 @@ service SystemService { permission: "authenticated"; }; } + + // Set a feature flag on an instance + rpc SetInstanceFeature(SetInstanceFeatureRequest) returns (SetInstanceFeatureResponse) { + option (google.api.http) = { + put: "/instances/{instance_id}/features/{feature_id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } } @@ -879,3 +891,19 @@ message FailedEvent { } ]; } + +message SetInstanceFeatureRequest { + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // the feature (id) according to [internal/domain/feature.go] + int32 feature_id = 2 [(validate.rules).int32 = {gt: 0}]; + // value based on the feature type + oneof value { + option (validate.required) = true; + + bool bool = 3; + } +} + +message SetInstanceFeatureResponse { + zitadel.v1.ObjectDetails details = 1; +} \ No newline at end of file