feat(login): use default org for login without provided org context (#6625)

* start feature flags

* base feature events on domain const

* setup default features

* allow setting feature in system api

* allow setting feature in admin api

* set settings in login based on feature

* fix rebasing

* unit tests

* i18n

* update policy after domain discovery

* some changes from review

* check feature and value type

* check feature and value type
This commit is contained in:
Livio Spring 2023-09-29 10:21:32 +02:00 committed by GitHub
parent d01f4d229f
commit 68bfab2fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 875 additions and 38 deletions

View File

@ -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"

View File

@ -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")

View File

@ -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

View File

@ -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")

View File

@ -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")

View File

@ -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)
}

45
feature/check.go Normal file
View File

@ -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
}

4
go.mod
View File

@ -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
)

8
go.sum
View File

@ -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=

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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{

View File

@ -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()

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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,
},
}
}

View File

@ -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]])
}

View File

@ -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,
}
}

View File

@ -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: Добавен потребител

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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;
}

View File

@ -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;
}