mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
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:
parent
d01f4d229f
commit
68bfab2fb3
@ -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"
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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
45
feature/check.go
Normal 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
4
go.mod
@ -87,6 +87,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect
|
||||
github.com/dmarkham/enumer v1.5.8 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
@ -101,9 +102,12 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
|
||||
github.com/pascaldekloe/name v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/smartystreets/assertions v1.0.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
)
|
||||
|
8
go.sum
8
go.sum
@ -179,6 +179,8 @@ github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwu
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM=
|
||||
github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog=
|
||||
@ -709,6 +711,8 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U=
|
||||
github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
@ -989,6 +993,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -1222,6 +1228,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
20
internal/api/grpc/admin/feature.go
Normal file
20
internal/api/grpc/admin/feature.go
Normal 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
|
||||
|
||||
}
|
34
internal/api/grpc/system/feature.go
Normal file
34
internal/api/grpc/system/feature.go
Normal 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")
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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{
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
63
internal/command/instance_feature.go
Normal file
63
internal/command/instance_feature.go
Normal 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
|
||||
}
|
||||
}
|
81
internal/command/instance_feature_model.go
Normal file
81
internal/command/instance_feature_model.go
Normal 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
|
||||
}
|
179
internal/command/instance_feature_test.go
Normal file
179
internal/command/instance_feature_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
27
internal/config/hook/feature.go
Normal file
27
internal/config/hook/feature.go
Normal 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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
28
internal/domain/feature.go
Normal file
28
internal/domain/feature.go
Normal 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
|
||||
)
|
78
internal/domain/feature_enumer.go
Normal file
78
internal/domain/feature_enumer.go
Normal 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
|
||||
}
|
30
internal/repository/feature/aggregate.go
Normal file
30
internal/repository/feature/aggregate.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
9
internal/repository/feature/eventstore.go
Normal file
9
internal/repository/feature/eventstore.go
Normal 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]])
|
||||
}
|
65
internal/repository/feature/feature.go
Normal file
65
internal/repository/feature/feature.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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: Добавен потребител
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user