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
41 changed files with 875 additions and 38 deletions

View File

@@ -761,6 +761,8 @@ DefaultInstance:
Greeting: Hello {{.DisplayName}}, 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. 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 ButtonText: Login
Features:
- FeatureLoginDefaultOrg: true
Quotas: Quotas:
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist. # Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
@@ -819,6 +821,7 @@ InternalAuthZ:
- "iam.flow.read" - "iam.flow.read"
- "iam.flow.write" - "iam.flow.write"
- "iam.flow.delete" - "iam.flow.delete"
- "iam.feature.write"
- "org.read" - "org.read"
- "org.global.read" - "org.global.read"
- "org.create" - "org.create"

View File

@@ -23,6 +23,7 @@ func MustNewConfig(v *viper.Viper) *Config {
mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","), mapstructure.StringToSliceHookFunc(","),
hook.StringToFeatureHookFunc(),
)), )),
) )
logging.OnError(err).Fatal("unable to read default config") logging.OnError(err).Fatal("unable to read default config")

View File

@@ -24,6 +24,7 @@ type FirstInstance struct {
Org command.InstanceOrgSetup Org command.InstanceOrgSetup
MachineKeyPath string MachineKeyPath string
PatPath string PatPath string
Features map[domain.Feature]any
instanceSetup command.InstanceSetup instanceSetup command.InstanceSetup
userEncryptionKey *crypto.KeyConfig userEncryptionKey *crypto.KeyConfig

View File

@@ -43,6 +43,7 @@ func MustNewConfig(v *viper.Viper) *Config {
mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","), mapstructure.StringToSliceHookFunc(","),
database.DecodeHook, database.DecodeHook,
hook.StringToFeatureHookFunc(),
)), )),
) )
logging.OnError(err).Fatal("unable to read default config") logging.OnError(err).Fatal("unable to read default config")
@@ -99,6 +100,7 @@ func MustNewSteps(v *viper.Viper) *Steps {
mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","), mapstructure.StringToSliceHookFunc(","),
hook.StringToFeatureHookFunc(),
)), )),
) )
logging.OnError(err).Fatal("unable to read steps") logging.OnError(err).Fatal("unable to read steps")

View File

@@ -92,6 +92,7 @@ func MustNewConfig(v *viper.Viper) *Config {
database.DecodeHook, database.DecodeHook,
actions.HTTPConfigDecodeHook, actions.HTTPConfigDecodeHook,
systemAPIUsersDecodeHook, systemAPIUsersDecodeHook,
hook.StringToFeatureHookFunc(),
)), )),
) )
logging.OnError(err).Fatal("unable to read config") 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/build"
"github.com/zitadel/zitadel/cmd/key" "github.com/zitadel/zitadel/cmd/key"
cmd_tls "github.com/zitadel/zitadel/cmd/tls" cmd_tls "github.com/zitadel/zitadel/cmd/tls"
"github.com/zitadel/zitadel/feature"
"github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/actions"
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api"
@@ -412,7 +413,27 @@ func startAPIs(
} }
apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c) 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 { if err != nil {
return fmt.Errorf("unable to start login: %w", err) 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 ( require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect 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/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.6.0 // 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-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/muhlemmer/httpforwarded v0.1.0 // 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/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/smartystreets/assertions v1.0.0 // indirect github.com/smartystreets/assertions v1.0.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // 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 v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc 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.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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-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-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog= 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/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/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/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/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 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 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.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.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.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-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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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.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-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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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/gorilla/mux"
"github.com/rakyll/statik/fs" "github.com/rakyll/statik/fs"
"github.com/zitadel/zitadel/feature"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http" http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/http/middleware"
@@ -40,6 +41,7 @@ type Login struct {
samlAuthCallbackURL func(context.Context, string) string samlAuthCallbackURL func(context.Context, string) string
idpConfigAlg crypto.EncryptionAlgorithm idpConfigAlg crypto.EncryptionAlgorithm
userCodeAlg crypto.EncryptionAlgorithm userCodeAlg crypto.EncryptionAlgorithm
featureCheck feature.Checker
} }
type Config struct { type Config struct {
@@ -76,6 +78,7 @@ func CreateLogin(config Config,
userCodeAlg crypto.EncryptionAlgorithm, userCodeAlg crypto.EncryptionAlgorithm,
idpConfigAlg crypto.EncryptionAlgorithm, idpConfigAlg crypto.EncryptionAlgorithm,
csrfCookieKey []byte, csrfCookieKey []byte,
featureCheck feature.Checker,
) (*Login, error) { ) (*Login, error) {
login := &Login{ login := &Login{
oidcAuthCallbackURL: oidcAuthCallbackURL, oidcAuthCallbackURL: oidcAuthCallbackURL,
@@ -88,6 +91,7 @@ func CreateLogin(config Config,
authRepo: authRepo, authRepo: authRepo,
idpConfigAlg: idpConfigAlg, idpConfigAlg: idpConfigAlg,
userCodeAlg: userCodeAlg, userCodeAlg: userCodeAlg,
featureCheck: featureCheck,
} }
statikFS, err := fs.NewWithNamespace("login") statikFS, err := fs.NewWithNamespace("login")
if err != nil { 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 { func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string {
privateLabelingOrgID := authz.GetInstance(r.Context()).InstanceID() defaultID := authz.GetInstance(r.Context()).DefaultOrganisationID()
if authReq == nil { f, err := l.featureCheck.CheckInstanceBooleanFeature(r.Context(), domain.FeatureLoginDefaultOrg)
if id := r.FormValue(queryOrgID); id != "" { logging.OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg)
return id if !f.Boolean {
} defaultID = authz.GetInstance(r.Context()).InstanceID()
return privateLabelingOrgID
} }
if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified { if authReq != nil {
privateLabelingOrgID = authReq.ApplicationResourceOwner return authReq.PrivateLabelingOrgID(defaultID)
} }
if authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingUnspecified { if id := r.FormValue(queryOrgID); id != "" {
if authReq.UserOrgID != "" { return id
privateLabelingOrgID = authReq.UserOrgID
}
} }
if authReq.RequestedOrgID != "" { return defaultID
privateLabelingOrgID = authReq.RequestedOrgID
}
return privateLabelingOrgID
} }
func (l *Login) getOrgName(authReq *domain.AuthRequest) string { func (l *Login) getOrgName(authReq *domain.AuthRequest) string {

View File

@@ -7,6 +7,7 @@ import (
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/feature"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
cache "github.com/zitadel/zitadel/internal/auth_request/repository" cache "github.com/zitadel/zitadel/internal/auth_request/repository"
@@ -52,6 +53,8 @@ type AuthRequestRepo struct {
ApplicationProvider applicationProvider ApplicationProvider applicationProvider
CustomTextProvider customTextProvider CustomTextProvider customTextProvider
FeatureCheck feature.Checker
IdGenerator id.Generator IdGenerator id.Generator
} }
@@ -650,7 +653,12 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A
orgID = request.UserOrgID orgID = request.UserOrgID
} }
if orgID == "" { 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) loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, orgID)
@@ -671,19 +679,7 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A
return err return err
} }
request.PrivacyPolicy = privacyPolicy request.PrivacyPolicy = privacyPolicy
privateLabelingOrgID := authz.GetInstance(ctx).InstanceID() labelPolicy, err := repo.getLabelPolicy(ctx, request.PrivateLabelingOrgID(orgID))
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)
if err != nil { if err != nil {
return err return err
} }
@@ -749,8 +745,9 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
} }
// the user was either not found or not active // the user was either not found or not active
// so check if the loginname suffix matches a verified org domain // so check if the loginname suffix matches a verified org domain
if repo.checkDomainDiscovery(ctx, request, loginName) { ok, err := repo.checkDomainDiscovery(ctx, request, loginName)
return nil if err != nil || ok {
return err
} }
// let's once again check if the user was just inactive // let's once again check if the user was just inactive
if user != nil && user.State == int32(domain.UserStateInactive) { 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") 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 // check if there's a suffix in the loginname
loginName = strings.TrimSpace(strings.ToLower(loginName)) loginName = strings.TrimSpace(strings.ToLower(loginName))
index := strings.LastIndex(loginName, "@") index := strings.LastIndex(loginName, "@")
if index < 0 { if index < 0 {
return false return false, nil
} }
// check if the suffix matches a verified domain // check if the suffix matches a verified domain
org, err := repo.Query.OrgByVerifiedDomain(ctx, loginName[index+1:]) org, err := repo.Query.OrgByVerifiedDomain(ctx, loginName[index+1:])
if err != nil { if err != nil {
return false return false, nil
} }
// and if the login policy allows domain discovery // and if the login policy allows domain discovery
policy, err := repo.Query.LoginPolicyByID(ctx, true, org.ID, false) policy, err := repo.Query.LoginPolicyByID(ctx, true, org.ID, false)
if err != nil || !policy.AllowDomainDiscovery { if err != nil || !policy.AllowDomainDiscovery {
return false return false, nil
} }
// discovery was allowed, so set the org as requested org // 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) // 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.SetOrgInformation(org.ID, org.Name, org.Domain, false)
request.SetUserInfo("", "", "", "", "", org.ID) request.SetUserInfo("", "", "", "", "", org.ID)
if err = repo.fillPolicies(ctx, request); err != nil {
return false, err
}
request.LoginHint = loginName request.LoginHint = loginName
request.Prompt = append(request.Prompt, domain.PromptCreate) // to trigger registration 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) { 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 ( import (
"context" "context"
"github.com/zitadel/zitadel/feature"
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/eventstore" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/eventstore"
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/spooler" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/spooler"
auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view" 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, ProjectProvider: queryView,
ApplicationProvider: queries, ApplicationProvider: queries,
CustomTextProvider: queries, CustomTextProvider: queries,
FeatureCheck: feature.NewCheck(esV2),
IdGenerator: idGenerator, IdGenerator: idGenerator,
}, },
eventstore.TokenRepo{ eventstore.TokenRepo{

View File

@@ -16,6 +16,7 @@ import (
"github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/action" "github.com/zitadel/zitadel/internal/repository/action"
"github.com/zitadel/zitadel/internal/repository/authrequest" "github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/feature"
"github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/idpintent"
instance_repo "github.com/zitadel/zitadel/internal/repository/instance" instance_repo "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/keypair" "github.com/zitadel/zitadel/internal/repository/keypair"
@@ -145,6 +146,7 @@ func StartCommands(
authrequest.RegisterEventMappers(repo.eventstore) authrequest.RegisterEventMappers(repo.eventstore)
oidcsession.RegisterEventMappers(repo.eventstore) oidcsession.RegisterEventMappers(repo.eventstore)
milestone.RegisterEventMappers(repo.eventstore) milestone.RegisterEventMappers(repo.eventstore)
feature.RegisterEventMappers(repo.eventstore)
repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost) repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher() repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher()

View File

@@ -15,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/repository/feature"
"github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/project"
@@ -112,6 +113,7 @@ type InstanceSetup struct {
Quotas *struct { Quotas *struct {
Items []*SetQuota Items []*SetQuota
} }
Features map[domain.Feature]any
} }
type SecretGenerators struct { 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...) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
if err != nil { if err != nil {
return "", "", nil, nil, err 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" "github.com/zitadel/zitadel/internal/eventstore/repository/mock"
action_repo "github.com/zitadel/zitadel/internal/repository/action" action_repo "github.com/zitadel/zitadel/internal/repository/action"
"github.com/zitadel/zitadel/internal/repository/authrequest" "github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/feature"
"github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/idpintent"
iam_repo "github.com/zitadel/zitadel/internal/repository/instance" iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
key_repo "github.com/zitadel/zitadel/internal/repository/keypair" 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) authrequest.RegisterEventMappers(es)
oidcsession.RegisterEventMappers(es) oidcsession.RegisterEventMappers(es)
quota_repo.RegisterEventMappers(es) quota_repo.RegisterEventMappers(es)
feature.RegisterEventMappers(es)
return 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 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: Token:
Invalid: Токенът е невалиден Invalid: Токенът е невалиден
Expired: Токенът е изтекъл Expired: Токенът е изтекъл
Feature:
NotExisting: Функцията не съществува
TypeNotSupported: Типът функция не се поддържа
InvalidValue: Невалидна стойност за тази функция
AggregateTypes: AggregateTypes:
action: Действие action: Действие
@@ -532,6 +536,8 @@ AggregateTypes:
user: Потребител user: Потребител
usergrant: Предоставяне на потребител usergrant: Предоставяне на потребител
quota: Квота quota: Квота
feature: Особеност
EventTypes: EventTypes:
user: user:
added: Добавен потребител added: Добавен потребител

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: Token ist ungültig Invalid: Token ist ungültig
Expired: Token ist abgelaufen Expired: Token ist abgelaufen
InvalidClient: Token wurde nicht für diesen Client ausgestellt 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: AggregateTypes:
action: Action action: Action
@@ -515,6 +519,7 @@ AggregateTypes:
user: Benutzer user: Benutzer
usergrant: Benutzerberechtigung usergrant: Benutzerberechtigung
quota: Kontingent quota: Kontingent
feature: Feature
EventTypes: EventTypes:
user: user:

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: Token is invalid Invalid: Token is invalid
Expired: Token is expired Expired: Token is expired
InvalidClient: Token was not issued for this client 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: AggregateTypes:
action: Action action: Action
@@ -515,6 +519,7 @@ AggregateTypes:
user: User user: User
usergrant: User grant usergrant: User grant
quota: Quota quota: Quota
feature: Feature
EventTypes: EventTypes:
user: user:

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: El token no es válido Invalid: El token no es válido
Expired: El token ha caducado Expired: El token ha caducado
InvalidClient: El token no ha sido emitido para este cliente 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: AggregateTypes:
action: Acción action: Acción
@@ -515,6 +519,7 @@ AggregateTypes:
user: Usuario user: Usuario
usergrant: Concesión de usuario usergrant: Concesión de usuario
quota: Cuota quota: Cuota
feature: Característica
EventTypes: EventTypes:
user: user:

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: Le jeton n'est pas valide Invalid: Le jeton n'est pas valide
Expired: Le jeton est expiré Expired: Le jeton est expiré
InvalidClient: Le token n'a pas été émis pour ce client 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: AggregateTypes:
action: Action action: Action
@@ -515,6 +519,7 @@ AggregateTypes:
user: Utilisateur user: Utilisateur
usergrant: Subvention de l'utilisateur usergrant: Subvention de l'utilisateur
quota: Contingent quota: Contingent
feature: Fonctionnalité
EventTypes: EventTypes:
user: user:

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: Token non è valido Invalid: Token non è valido
Expired: Token è scaduto Expired: Token è scaduto
InvalidClient: Il token non è stato emesso per questo cliente 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: AggregateTypes:
action: Azione action: Azione
@@ -515,6 +519,7 @@ AggregateTypes:
user: Utente user: Utente
usergrant: Sovvenzione utente usergrant: Sovvenzione utente
quota: Quota quota: Quota
feature: Funzionalità
EventTypes: EventTypes:
user: user:

View File

@@ -494,6 +494,10 @@ Errors:
Invalid: トークンが無効です Invalid: トークンが無効です
Expired: トークンの有効期限が切れている Expired: トークンの有効期限が切れている
InvalidClient: トークンが発行されていません InvalidClient: トークンが発行されていません
Feature:
NotExisting: 機能が存在しません
TypeNotSupported: 機能タイプはサポートされていません
InvalidValue: この機能には無効な値です
AggregateTypes: AggregateTypes:
action: アクション action: アクション
@@ -504,6 +508,7 @@ AggregateTypes:
user: ユーザー user: ユーザー
usergrant: ユーザーグラント usergrant: ユーザーグラント
quota: クォータ quota: クォータ
feature: 特徴
EventTypes: EventTypes:
user: user:

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: токенот е неважечки Invalid: токенот е неважечки
Expired: токенот е истечен Expired: токенот е истечен
InvalidClient: Токен не беше издаден на овој клиент InvalidClient: Токен не беше издаден на овој клиент
Feature:
NotExisting: Функцијата не постои
TypeNotSupported: Типот на функција не е поддржан
InvalidValue: Неважечка вредност за оваа функција
AggregateTypes: AggregateTypes:
action: Акција action: Акција
@@ -515,6 +519,7 @@ AggregateTypes:
user: Корисник user: Корисник
usergrant: Овластување на корисник usergrant: Овластување на корисник
quota: Квота quota: Квота
feature: Карактеристика
EventTypes: EventTypes:
user: user:

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: Token jest nieprawidłowy Invalid: Token jest nieprawidłowy
Expired: Token wygasł Expired: Token wygasł
InvalidClient: Token nie został wydany dla tego klienta 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: AggregateTypes:
action: Działanie action: Działanie
@@ -515,6 +519,7 @@ AggregateTypes:
user: Użytkownik user: Użytkownik
usergrant: Uprawnienie użytkownika usergrant: Uprawnienie użytkownika
quota: Limit quota: Limit
feature: Funkcja
EventTypes: EventTypes:
user: user:

View File

@@ -499,6 +499,10 @@ Errors:
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
OIDCSession: OIDCSession:
RefreshTokenInvalid: O Refresh Token é inválido 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: AggregateTypes:
action: Ação action: Ação
@@ -509,6 +513,7 @@ AggregateTypes:
user: Usuário user: Usuário
usergrant: Concessão de usuário usergrant: Concessão de usuário
quota: Cota quota: Cota
feature: Recurso
EventTypes: EventTypes:
user: user:

View File

@@ -505,6 +505,10 @@ Errors:
Invalid: 令牌无效 Invalid: 令牌无效
Expired: 令牌已过期 Expired: 令牌已过期
InvalidClient: 没有为该客户发放令牌 InvalidClient: 没有为该客户发放令牌
Feature:
NotExisting: 功能不存在
TypeNotSupported: 不支持功能类型
InvalidValue: 此功能的值无效
AggregateTypes: AggregateTypes:
action: 动作 action: 动作
@@ -515,6 +519,7 @@ AggregateTypes:
user: 用户 user: 用户
usergrant: 用户授权 usergrant: 用户授权
quota: 配额 quota: 配额
feature: 特征
EventTypes: EventTypes:
user: 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." 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 { message ListAggregateTypesResponse {
repeated zitadel.event.v1.AggregateType aggregate_types = 1; 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"; 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;
}