mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:57:31 +00:00
feat: features (#1427)
* features * features * features * fix json tags * add features handler to auth * mocks for tests * add setup step * fixes * add featurelist to auth api * grandfather state and typos * typo * merge new-eventstore * fix login policy tests * label policy in features * audit log retention
This commit is contained in:
@@ -4,6 +4,8 @@ InternalAuthZ:
|
||||
Permissions:
|
||||
- "iam.read"
|
||||
- "iam.write"
|
||||
- "iam.features.read"
|
||||
- "iam.features.write"
|
||||
- "iam.policy.read"
|
||||
- "iam.policy.write"
|
||||
- "iam.policy.delete"
|
||||
@@ -31,6 +33,7 @@ InternalAuthZ:
|
||||
- "user.grant.write"
|
||||
- "user.grant.delete"
|
||||
- "user.membership.read"
|
||||
- "features.read"
|
||||
- "policy.read"
|
||||
- "policy.write"
|
||||
- "policy.delete"
|
||||
@@ -56,6 +59,7 @@ InternalAuthZ:
|
||||
- Role: 'IAM_OWNER_VIEWER'
|
||||
Permissions:
|
||||
- "iam.read"
|
||||
- "iam.features.read"
|
||||
- "iam.policy.read"
|
||||
- "iam.member.read"
|
||||
- "iam.idp.read"
|
||||
@@ -66,6 +70,7 @@ InternalAuthZ:
|
||||
- "user.global.read"
|
||||
- "user.grant.read"
|
||||
- "user.membership.read"
|
||||
- "features.read"
|
||||
- "policy.read"
|
||||
- "project.read"
|
||||
- "project.member.read"
|
||||
@@ -93,6 +98,7 @@ InternalAuthZ:
|
||||
- "user.grant.write"
|
||||
- "user.grant.delete"
|
||||
- "user.membership.read"
|
||||
- "features.read"
|
||||
- "policy.read"
|
||||
- "policy.write"
|
||||
- "policy.delete"
|
||||
@@ -123,6 +129,7 @@ InternalAuthZ:
|
||||
- "user.global.read"
|
||||
- "user.grant.read"
|
||||
- "user.membership.read"
|
||||
- "features.read"
|
||||
- "policy.read"
|
||||
- "project.read"
|
||||
- "project.member.read"
|
||||
|
@@ -103,24 +103,24 @@ func startZitadel(configPaths []string) {
|
||||
logging.Log("MAIN-FaF2r").OnError(err).Fatal("cannot read config")
|
||||
|
||||
ctx := context.Background()
|
||||
esCommands, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Commands.Eventstore)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
commands, err := command.StartCommands(esCommands, conf.SystemDefaults, conf.InternalAuthZ)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
esQueries, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Queries.Eventstore)
|
||||
if err != nil {
|
||||
return
|
||||
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start eventstore for queries")
|
||||
}
|
||||
queries, err := query.StartQueries(esQueries, conf.SystemDefaults)
|
||||
if err != nil {
|
||||
return
|
||||
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start queries")
|
||||
}
|
||||
authZRepo, err := authz.Start(ctx, conf.AuthZ, conf.InternalAuthZ, conf.SystemDefaults, queries)
|
||||
logging.Log("MAIN-s9KOw").OnError(err).Fatal("error starting authz repo")
|
||||
esCommands, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Commands.Eventstore)
|
||||
if err != nil {
|
||||
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start eventstore for commands")
|
||||
}
|
||||
commands, err := command.StartCommands(esCommands, conf.SystemDefaults, conf.InternalAuthZ, authZRepo)
|
||||
if err != nil {
|
||||
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start commands")
|
||||
}
|
||||
var authRepo *auth_es.EsRepository
|
||||
if *authEnabled || *oidcEnabled || *loginEnabled {
|
||||
authRepo, err = auth_es.Start(conf.Auth, conf.InternalAuthZ, conf.SystemDefaults, commands, queries, authZRepo, esQueries)
|
||||
@@ -190,7 +190,7 @@ func startSetup(configPaths []string, localDevMode bool) {
|
||||
es, err := eventstore.Start(conf.Eventstore)
|
||||
logging.Log("MAIN-Ddt3").OnError(err).Fatal("cannot start eventstore")
|
||||
|
||||
commands, err := command.StartCommands(es, conf.SystemDefaults, conf.InternalAuthZ)
|
||||
commands, err := command.StartCommands(es, conf.SystemDefaults, conf.InternalAuthZ, nil)
|
||||
logging.Log("MAIN-dsjrr").OnError(err).Fatal("cannot start command side")
|
||||
|
||||
err = setup.Execute(ctx, conf.SetUp, conf.SystemDefaults.IamID, commands)
|
||||
|
@@ -175,3 +175,6 @@ SetUp:
|
||||
ButtonText: Login
|
||||
Step11:
|
||||
MigrateV1EventstoreToV2: $ZITADEL_MIGRATE_ES_V1
|
||||
Step12:
|
||||
TierName: FREE Tier
|
||||
AuditLogRetention: 9600h #400d = ~13months
|
||||
|
@@ -0,0 +1,70 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
admin_view "github.com/caos/zitadel/internal/admin/repository/eventsourcing/view"
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
v1 "github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
||||
)
|
||||
|
||||
type FeaturesRepo struct {
|
||||
Eventstore v1.Eventstore
|
||||
|
||||
View *admin_view.View
|
||||
|
||||
SearchLimit uint64
|
||||
SystemDefaults systemdefaults.SystemDefaults
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) GetDefaultFeatures(ctx context.Context) (*features_model.FeaturesView, error) {
|
||||
features, viewErr := repo.View.FeaturesByAggregateID(domain.IAMID)
|
||||
if viewErr != nil && !errors.IsNotFound(viewErr) {
|
||||
return nil, viewErr
|
||||
}
|
||||
if errors.IsNotFound(viewErr) {
|
||||
features = new(model.FeaturesView)
|
||||
}
|
||||
events, esErr := repo.getIAMEvents(ctx, features.Sequence)
|
||||
if errors.IsNotFound(viewErr) && len(events) == 0 {
|
||||
return nil, errors.ThrowNotFound(nil, "EVENT-Lsoj7", "Errors.Org.NotFound")
|
||||
}
|
||||
if esErr != nil {
|
||||
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
|
||||
return model.FeaturesToModel(features), nil
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return model.FeaturesToModel(&featuresCopy), nil
|
||||
}
|
||||
}
|
||||
return model.FeaturesToModel(&featuresCopy), nil
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) GetOrgFeatures(ctx context.Context, orgID string) (*features_model.FeaturesView, error) {
|
||||
features, err := repo.View.FeaturesByAggregateID(orgID)
|
||||
if errors.IsNotFound(err) {
|
||||
return repo.GetDefaultFeatures(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.FeaturesToModel(features), nil
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) {
|
||||
query, err := iam_view.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.Eventstore.FilterEvents(ctx, query)
|
||||
}
|
165
internal/admin/repository/eventsourcing/handler/features.go
Normal file
165
internal/admin/repository/eventsourcing/handler/features.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
iam_repo "github.com/caos/zitadel/internal/repository/iam"
|
||||
org_repo "github.com/caos/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "adminapi.features"
|
||||
)
|
||||
|
||||
type Features struct {
|
||||
handler
|
||||
subscription *v1.Subscription
|
||||
}
|
||||
|
||||
func newFeatures(handler handler) *Features {
|
||||
h := &Features{
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (p *Features) subscribe() {
|
||||
p.subscription = p.es.Subscribe(p.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range p.subscription.Events {
|
||||
query.ReduceEvent(p, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Features) ViewModel() string {
|
||||
return featuresTable
|
||||
}
|
||||
|
||||
func (p *Features) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{iam_es_model.IAMAggregate, org_es_model.OrgAggregate}
|
||||
}
|
||||
|
||||
func (p *Features) EventQuery() (*es_models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(p.AggregateTypes()...).
|
||||
LatestSequenceFilter(sequence.CurrentSequence), nil
|
||||
}
|
||||
|
||||
func (p *Features) CurrentSequence() (uint64, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (p *Features) Reduce(event *es_models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case org_es_model.OrgAggregate, iam_es_model.IAMAggregate:
|
||||
err = p.processFeatures(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Features) processFeatures(event *es_models.Event) (err error) {
|
||||
features := new(model.FeaturesView)
|
||||
switch string(event.Type) {
|
||||
case string(org_es_model.OrgAdded):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
case string(iam_repo.FeaturesSetEventType):
|
||||
defaultFeatures, err := p.view.AllDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, features := range defaultFeatures {
|
||||
err = features.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return p.view.PutFeaturesList(defaultFeatures, event)
|
||||
case string(org_repo.FeaturesSetEventType):
|
||||
features, err = p.view.FeaturesByAggregateID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = features.AppendEvent(event)
|
||||
case string(org_repo.FeaturesRemovedEventType):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
default:
|
||||
return p.view.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutFeatures(features, event)
|
||||
}
|
||||
|
||||
func (p *Features) OnError(event *es_models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-Wj8sf", "id", event.AggregateID).WithError(err).Warn("something went wrong in login features handler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestFeaturesFailedEvent, p.view.ProcessedFeaturesFailedEvent, p.view.ProcessedFeaturesSequence, p.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (p *Features) OnSuccess() error {
|
||||
return spooler.HandleSuccess(p.view.UpdateFeaturesSpoolerRunTimestamp)
|
||||
}
|
||||
|
||||
func (p *Features) getDefaultFeatures() (*model.FeaturesView, error) {
|
||||
features, featuresErr := p.view.FeaturesByAggregateID(domain.IAMID)
|
||||
if featuresErr != nil && !caos_errs.IsNotFound(featuresErr) {
|
||||
return nil, featuresErr
|
||||
}
|
||||
if features == nil {
|
||||
features = &model.FeaturesView{}
|
||||
}
|
||||
events, err := p.getIAMEvents(features.Sequence)
|
||||
if err != nil {
|
||||
return features, featuresErr
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return features, nil
|
||||
}
|
||||
}
|
||||
return &featuresCopy, nil
|
||||
}
|
||||
|
||||
func (p *Features) getIAMEvents(sequence uint64) ([]*es_models.Event, error) {
|
||||
query, err := eventsourcing.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.es.FilterEvents(context.Background(), query)
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
|
||||
"github.com/caos/zitadel/internal/admin/repository/eventsourcing/view"
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
@@ -62,6 +63,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
|
||||
handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}),
|
||||
newMailText(
|
||||
handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}),
|
||||
newFeatures(
|
||||
handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -24,6 +24,7 @@ type EsRepository struct {
|
||||
eventstore.OrgRepo
|
||||
eventstore.IAMRepository
|
||||
eventstore.AdministratorRepo
|
||||
eventstore.FeaturesRepo
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, roles []string) (*EsRepository, error) {
|
||||
@@ -60,6 +61,12 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, r
|
||||
AdministratorRepo: eventstore.AdministratorRepo{
|
||||
View: view,
|
||||
},
|
||||
FeaturesRepo: eventstore.FeaturesRepo{
|
||||
Eventstore: es,
|
||||
View: view,
|
||||
SearchLimit: conf.SearchLimit,
|
||||
SystemDefaults: systemDefaults,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
56
internal/admin/repository/eventsourcing/view/features.go
Normal file
56
internal/admin/repository/eventsourcing/view/features.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/features/repository/view"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "adminapi.features"
|
||||
)
|
||||
|
||||
func (v *View) AllDefaultFeatures() ([]*model.FeaturesView, error) {
|
||||
return view.GetDefaultFeatures(v.Db, featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) FeaturesByAggregateID(aggregateID string) (*model.FeaturesView, error) {
|
||||
return view.GetFeaturesByAggregateID(v.Db, featuresTable, aggregateID)
|
||||
}
|
||||
|
||||
func (v *View) PutFeatures(features *model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeatures(v.Db, featuresTable, features)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) PutFeaturesList(features []*model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeaturesList(v.Db, featuresTable, features...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesSequence() (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(featuresTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateFeaturesSpoolerRunTimestamp() error {
|
||||
return v.updateSpoolerRunSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(featuresTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
12
internal/admin/repository/features.go
Normal file
12
internal/admin/repository/features.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
)
|
||||
|
||||
type FeaturesRepository interface {
|
||||
GetDefaultFeatures(ctx context.Context) (*features_model.FeaturesView, error)
|
||||
GetOrgFeatures(ctx context.Context, id string) (*features_model.FeaturesView, error)
|
||||
}
|
@@ -7,4 +7,5 @@ type Repository interface {
|
||||
OrgRepository
|
||||
IAMRepository
|
||||
AdministratorRepository
|
||||
FeaturesRepository
|
||||
}
|
||||
|
@@ -23,6 +23,13 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if requiredAuthOption.Feature != "" {
|
||||
err = CheckOrgFeatures(ctx, verifier, ctxData.OrgID, requiredAuthOption.Feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if requiredAuthOption.Permission == authenticated {
|
||||
return func(parent context.Context) context.Context {
|
||||
return context.WithValue(parent, dataKey, ctxData)
|
||||
@@ -49,6 +56,10 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
|
||||
}, nil
|
||||
}
|
||||
|
||||
func CheckOrgFeatures(ctx context.Context, t *TokenVerifier, orgID string, requiredFeatures ...string) error {
|
||||
return t.authZRepo.CheckOrgFeatures(ctx, orgID, requiredFeatures...)
|
||||
}
|
||||
|
||||
func checkUserPermissions(req interface{}, userPerms []string, authOpt Option) error {
|
||||
if len(userPerms) == 0 {
|
||||
return errors.ThrowPermissionDenied(nil, "AUTH-5mWD2", "No matching permissions found")
|
||||
|
@@ -14,6 +14,7 @@ type MethodMapping map[string]Option
|
||||
type Option struct {
|
||||
Permission string
|
||||
CheckParam string
|
||||
Feature string
|
||||
}
|
||||
|
||||
func (a *Config) getPermissionsFromRole(role string) []string {
|
||||
|
@@ -34,6 +34,10 @@ func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (st
|
||||
return "clientID", nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) CheckOrgFeatures(context.Context, string, ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func equalStringArray(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
|
@@ -25,6 +25,7 @@ type authZRepo interface {
|
||||
SearchMyMemberships(ctx context.Context) ([]*Membership, error)
|
||||
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
||||
ExistsOrg(ctx context.Context, orgID string) error
|
||||
CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error
|
||||
}
|
||||
|
||||
func Start(authZRepo authZRepo) (v *TokenVerifier) {
|
||||
|
88
internal/api/grpc/admin/features.go
Normal file
88
internal/api/grpc/admin/features.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
features_grpc "github.com/caos/zitadel/internal/api/grpc/features"
|
||||
object_grpc "github.com/caos/zitadel/internal/api/grpc/object"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
admin_pb "github.com/caos/zitadel/pkg/grpc/admin"
|
||||
)
|
||||
|
||||
func (s *Server) GetDefaultFeatures(ctx context.Context, _ *admin_pb.GetDefaultFeaturesRequest) (*admin_pb.GetDefaultFeaturesResponse, error) {
|
||||
features, err := s.features.GetDefaultFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &admin_pb.GetDefaultFeaturesResponse{
|
||||
Features: features_grpc.FeaturesFromModel(features),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetDefaultFeatures(ctx context.Context, req *admin_pb.SetDefaultFeaturesRequest) (*admin_pb.SetDefaultFeaturesResponse, error) {
|
||||
details, err := s.command.SetDefaultFeatures(ctx, setDefaultFeaturesRequestToDomain(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &admin_pb.SetDefaultFeaturesResponse{
|
||||
Details: object_grpc.DomainToChangeDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetOrgFeatures(ctx context.Context, req *admin_pb.GetOrgFeaturesRequest) (*admin_pb.GetOrgFeaturesResponse, error) {
|
||||
features, err := s.features.GetOrgFeatures(ctx, req.OrgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &admin_pb.GetOrgFeaturesResponse{
|
||||
Features: features_grpc.FeaturesFromModel(features),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetOrgFeatures(ctx context.Context, req *admin_pb.SetOrgFeaturesRequest) (*admin_pb.SetOrgFeaturesResponse, error) {
|
||||
details, err := s.command.SetOrgFeatures(ctx, req.OrgId, setOrgFeaturesRequestToDomain(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &admin_pb.SetOrgFeaturesResponse{
|
||||
Details: object_grpc.DomainToChangeDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetOrgFeatures(ctx context.Context, req *admin_pb.ResetOrgFeaturesRequest) (*admin_pb.ResetOrgFeaturesResponse, error) {
|
||||
details, err := s.command.RemoveOrgFeatures(ctx, req.OrgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &admin_pb.ResetOrgFeaturesResponse{
|
||||
Details: object_grpc.DomainToChangeDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) *domain.Features {
|
||||
return &domain.Features{
|
||||
TierName: req.TierName,
|
||||
TierDescription: req.Description,
|
||||
AuditLogRetention: req.AuditLogRetention.AsDuration(),
|
||||
LoginPolicyFactors: req.LoginPolicyFactors,
|
||||
LoginPolicyIDP: req.LoginPolicyIdp,
|
||||
LoginPolicyPasswordless: req.LoginPolicyPasswordless,
|
||||
LoginPolicyRegistration: req.LoginPolicyRegistration,
|
||||
LoginPolicyUsernameLogin: req.LoginPolicyUsernameLogin,
|
||||
}
|
||||
}
|
||||
|
||||
func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain.Features {
|
||||
return &domain.Features{
|
||||
TierName: req.TierName,
|
||||
TierDescription: req.Description,
|
||||
State: features_grpc.FeaturesStateToDomain(req.State),
|
||||
StateDescription: req.StateDescription,
|
||||
AuditLogRetention: req.AuditLogRetention.AsDuration(),
|
||||
LoginPolicyFactors: req.LoginPolicyFactors,
|
||||
LoginPolicyIDP: req.LoginPolicyIdp,
|
||||
LoginPolicyPasswordless: req.LoginPolicyPasswordless,
|
||||
LoginPolicyRegistration: req.LoginPolicyRegistration,
|
||||
LoginPolicyUsernameLogin: req.LoginPolicyUsernameLogin,
|
||||
}
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/caos/zitadel/internal/admin/repository"
|
||||
"github.com/caos/zitadel/internal/admin/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
@@ -8,7 +10,6 @@ import (
|
||||
"github.com/caos/zitadel/internal/command"
|
||||
"github.com/caos/zitadel/internal/query"
|
||||
"github.com/caos/zitadel/pkg/grpc/admin"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,6 +26,7 @@ type Server struct {
|
||||
iam repository.IAMRepository
|
||||
administrator repository.AdministratorRepository
|
||||
repo repository.Repository
|
||||
features repository.FeaturesRepository
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -39,6 +41,7 @@ func CreateServer(command *command.Commands, query *query.Queries, repo reposito
|
||||
iam: repo,
|
||||
administrator: repo,
|
||||
repo: repo,
|
||||
features: repo,
|
||||
}
|
||||
}
|
||||
|
||||
|
18
internal/api/grpc/auth/features.go
Normal file
18
internal/api/grpc/auth/features.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
auth_pb "github.com/caos/zitadel/pkg/grpc/auth"
|
||||
)
|
||||
|
||||
func (s *Server) ListMyZitadelFeatures(ctx context.Context, _ *auth_pb.ListMyZitadelFeaturesRequest) (*auth_pb.ListMyZitadelFeaturesResponse, error) {
|
||||
features, err := s.repo.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth_pb.ListMyZitadelFeaturesResponse{
|
||||
Result: features.FeatureList(),
|
||||
}, nil
|
||||
}
|
@@ -24,7 +24,11 @@ func (s *Server) GetMyUser(ctx context.Context, _ *auth_pb.GetMyUserRequest) (*a
|
||||
|
||||
func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserChangesRequest) (*auth_pb.ListMyUserChangesResponse, error) {
|
||||
offset, limit, asc := object.ListQueryToModel(req.Query)
|
||||
changes, err := s.repo.MyUserChanges(ctx, offset, limit, asc)
|
||||
features, err := s.repo.GetOrgFeatures(ctx, authz.GetCtxData(ctx).ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changes, err := s.repo.MyUserChanges(ctx, offset, limit, asc, features.AuditLogRetention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
64
internal/api/grpc/features/features.go
Normal file
64
internal/api/grpc/features/features.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package features
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
object_grpc "github.com/caos/zitadel/internal/api/grpc/object"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
features_pb "github.com/caos/zitadel/pkg/grpc/features"
|
||||
)
|
||||
|
||||
func FeaturesFromModel(features *features_model.FeaturesView) *features_pb.Features {
|
||||
return &features_pb.Features{
|
||||
Details: object_grpc.ToViewDetailsPb(features.Sequence, features.CreationDate, features.ChangeDate, features.AggregateID),
|
||||
Tier: FeatureTierToPb(features.TierName, features.TierDescription, features.State, features.StateDescription),
|
||||
IsDefault: features.Default,
|
||||
|
||||
AuditLogRetention: durationpb.New(features.AuditLogRetention),
|
||||
LoginPolicyFactors: features.LoginPolicyFactors,
|
||||
LoginPolicyIdp: features.LoginPolicyIDP,
|
||||
LoginPolicyPasswordless: features.LoginPolicyPasswordless,
|
||||
LoginPolicyRegistration: features.LoginPolicyRegistration,
|
||||
LoginPolicyUsernameLogin: features.LoginPolicyUsernameLogin,
|
||||
}
|
||||
}
|
||||
|
||||
func FeatureTierToPb(name, description string, status domain.FeaturesState, statusDescription string) *features_pb.FeatureTier {
|
||||
return &features_pb.FeatureTier{
|
||||
Name: name,
|
||||
Description: description,
|
||||
State: FeaturesStateToPb(status),
|
||||
StatusInfo: statusDescription,
|
||||
}
|
||||
}
|
||||
|
||||
func FeaturesStateToPb(status domain.FeaturesState) features_pb.FeaturesState {
|
||||
switch status {
|
||||
case domain.FeaturesStateActive:
|
||||
return features_pb.FeaturesState_FEATURES_STATE_ACTIVE
|
||||
case domain.FeaturesStateActionRequired:
|
||||
return features_pb.FeaturesState_FEATURES_STATE_ACTION_REQUIRED
|
||||
case domain.FeaturesStateCanceled:
|
||||
return features_pb.FeaturesState_FEATURES_STATE_CANCELED
|
||||
case domain.FeaturesStateGrandfathered:
|
||||
return features_pb.FeaturesState_FEATURES_STATE_GRANDFATHERED
|
||||
default:
|
||||
return features_pb.FeaturesState_FEATURES_STATE_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
func FeaturesStateToDomain(status features_pb.FeaturesState) domain.FeaturesState {
|
||||
switch status {
|
||||
case features_pb.FeaturesState_FEATURES_STATE_ACTIVE:
|
||||
return domain.FeaturesStateActive
|
||||
case features_pb.FeaturesState_FEATURES_STATE_ACTION_REQUIRED:
|
||||
return domain.FeaturesStateActionRequired
|
||||
case features_pb.FeaturesState_FEATURES_STATE_CANCELED:
|
||||
return domain.FeaturesStateCanceled
|
||||
case features_pb.FeaturesState_FEATURES_STATE_GRANDFATHERED:
|
||||
return domain.FeaturesStateGrandfathered
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
19
internal/api/grpc/management/features.go
Normal file
19
internal/api/grpc/management/features.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
features_grpc "github.com/caos/zitadel/internal/api/grpc/features"
|
||||
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
|
||||
)
|
||||
|
||||
func (s *Server) GetFeatures(ctx context.Context, req *mgmt_pb.GetFeaturesRequest) (*mgmt_pb.GetFeaturesResponse, error) {
|
||||
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mgmt_pb.GetFeaturesResponse{
|
||||
Features: features_grpc.FeaturesFromModel(features),
|
||||
}, nil
|
||||
}
|
@@ -33,7 +33,11 @@ func (s *Server) GetOrgByDomainGlobal(ctx context.Context, req *mgmt_pb.GetOrgBy
|
||||
|
||||
func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChangesRequest) (*mgmt_pb.ListOrgChangesResponse, error) {
|
||||
offset, limit, asc := object.ListQueryToModel(req.Query)
|
||||
response, err := s.org.OrgChanges(ctx, authz.GetCtxData(ctx).OrgID, offset, limit, asc)
|
||||
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := s.org.OrgChanges(ctx, authz.GetCtxData(ctx).OrgID, offset, limit, asc, features.AuditLogRetention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -73,7 +73,11 @@ func (s *Server) ListGrantedProjects(ctx context.Context, req *mgmt_pb.ListGrant
|
||||
|
||||
func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjectChangesRequest) (*mgmt_pb.ListProjectChangesResponse, error) {
|
||||
offset, limit, asc := object_grpc.ListQueryToModel(req.Query)
|
||||
res, err := s.project.ProjectChanges(ctx, req.ProjectId, offset, limit, asc)
|
||||
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := s.project.ProjectChanges(ctx, req.ProjectId, offset, limit, asc, features.AuditLogRetention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -42,7 +42,11 @@ func (s *Server) ListApps(ctx context.Context, req *mgmt_pb.ListAppsRequest) (*m
|
||||
|
||||
func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChangesRequest) (*mgmt_pb.ListAppChangesResponse, error) {
|
||||
offset, limit, asc := object_grpc.ListQueryToModel(req.Query)
|
||||
res, err := s.project.ApplicationChanges(ctx, req.ProjectId, req.AppId, offset, limit, asc)
|
||||
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := s.project.ApplicationChanges(ctx, req.ProjectId, req.AppId, offset, limit, asc, features.AuditLogRetention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/api/grpc/server"
|
||||
"github.com/caos/zitadel/internal/command"
|
||||
@@ -9,7 +11,6 @@ import (
|
||||
"github.com/caos/zitadel/internal/management/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/query"
|
||||
"github.com/caos/zitadel/pkg/grpc/management"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,6 +28,7 @@ type Server struct {
|
||||
user repository.UserRepository
|
||||
usergrant repository.UserGrantRepository
|
||||
iam repository.IamRepository
|
||||
features repository.FeaturesRepository
|
||||
authZ authz.Config
|
||||
systemDefaults systemdefaults.SystemDefaults
|
||||
}
|
||||
@@ -44,6 +46,7 @@ func CreateServer(command *command.Commands, query *query.Queries, repo reposito
|
||||
user: repo,
|
||||
usergrant: repo,
|
||||
iam: repo,
|
||||
features: repo,
|
||||
systemDefaults: sd,
|
||||
}
|
||||
}
|
||||
|
@@ -53,7 +53,11 @@ func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) (
|
||||
|
||||
func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChangesRequest) (*mgmt_pb.ListUserChangesResponse, error) {
|
||||
offset, limit, asc := object.ListQueryToModel(req.Query)
|
||||
res, err := s.user.UserChanges(ctx, req.UserId, offset, limit, asc)
|
||||
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := s.user.UserChanges(ctx, req.UserId, offset, limit, asc, features.AuditLogRetention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ func ModelLoginPolicyToPb(policy *model.LoginPolicyView) *policy_pb.LoginPolicy
|
||||
IsDefault: policy.Default,
|
||||
AllowUsernamePassword: policy.AllowUsernamePassword,
|
||||
AllowRegister: policy.AllowRegister,
|
||||
AllowExternalIdp: policy.AllowRegister,
|
||||
AllowExternalIdp: policy.AllowExternalIDP,
|
||||
ForceMfa: policy.ForceMFA,
|
||||
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
|
||||
}
|
||||
|
@@ -37,6 +37,9 @@ func (v *verifierMock) ExistsOrg(ctx context.Context, orgID string) error {
|
||||
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (v *verifierMock) CheckOrgFeatures(context.Context, string, ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_authorize(t *testing.T) {
|
||||
type args struct {
|
||||
|
@@ -0,0 +1,66 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
auth_view "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
v1 "github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
||||
)
|
||||
|
||||
type FeaturesRepo struct {
|
||||
Eventstore v1.Eventstore
|
||||
|
||||
View *auth_view.View
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) GetDefaultFeatures(ctx context.Context) (*features_model.FeaturesView, error) {
|
||||
features, viewErr := repo.View.FeaturesByAggregateID(domain.IAMID)
|
||||
if viewErr != nil && !errors.IsNotFound(viewErr) {
|
||||
return nil, viewErr
|
||||
}
|
||||
if errors.IsNotFound(viewErr) {
|
||||
features = new(model.FeaturesView)
|
||||
}
|
||||
events, esErr := repo.getIAMEvents(ctx, features.Sequence)
|
||||
if errors.IsNotFound(viewErr) && len(events) == 0 {
|
||||
return nil, errors.ThrowNotFound(nil, "EVENT-Lsoj7", "Errors.Org.NotFound")
|
||||
}
|
||||
if esErr != nil {
|
||||
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
|
||||
return model.FeaturesToModel(features), nil
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return model.FeaturesToModel(&featuresCopy), nil
|
||||
}
|
||||
}
|
||||
return model.FeaturesToModel(&featuresCopy), nil
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) GetOrgFeatures(ctx context.Context, orgID string) (*features_model.FeaturesView, error) {
|
||||
features, err := repo.View.FeaturesByAggregateID(orgID)
|
||||
if errors.IsNotFound(err) {
|
||||
return repo.GetDefaultFeatures(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.FeaturesToModel(features), nil
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) {
|
||||
query, err := iam_view.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.Eventstore.FilterEvents(ctx, query)
|
||||
}
|
@@ -2,6 +2,8 @@ package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
|
||||
@@ -187,8 +189,8 @@ func (repo *UserRepo) UserByLoginName(ctx context.Context, loginname string) (*m
|
||||
}
|
||||
return usr_view_model.UserToModel(&userCopy), nil
|
||||
}
|
||||
func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error) {
|
||||
changes, err := repo.getUserChanges(ctx, authz.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending)
|
||||
func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) {
|
||||
changes, err := repo.getUserChanges(ctx, authz.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending, retention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,8 +217,8 @@ func (repo *UserRepo) MachineKeyByID(ctx context.Context, keyID string) (*key_mo
|
||||
return key_view_model.AuthNKeyToModel(key), nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error) {
|
||||
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending)
|
||||
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) {
|
||||
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention)
|
||||
|
||||
events, err := r.Eventstore.FilterEvents(ctx, query)
|
||||
if err != nil {
|
||||
|
165
internal/auth/repository/eventsourcing/handler/features.go
Normal file
165
internal/auth/repository/eventsourcing/handler/features.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
iam_repo "github.com/caos/zitadel/internal/repository/iam"
|
||||
org_repo "github.com/caos/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "auth.features"
|
||||
)
|
||||
|
||||
type Features struct {
|
||||
handler
|
||||
subscription *v1.Subscription
|
||||
}
|
||||
|
||||
func newFeatures(handler handler) *Features {
|
||||
h := &Features{
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (p *Features) subscribe() {
|
||||
p.subscription = p.es.Subscribe(p.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range p.subscription.Events {
|
||||
query.ReduceEvent(p, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Features) ViewModel() string {
|
||||
return featuresTable
|
||||
}
|
||||
|
||||
func (p *Features) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{iam_es_model.IAMAggregate, org_es_model.OrgAggregate}
|
||||
}
|
||||
|
||||
func (p *Features) EventQuery() (*es_models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(p.AggregateTypes()...).
|
||||
LatestSequenceFilter(sequence.CurrentSequence), nil
|
||||
}
|
||||
|
||||
func (p *Features) CurrentSequence() (uint64, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (p *Features) Reduce(event *es_models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case org_es_model.OrgAggregate, iam_es_model.IAMAggregate:
|
||||
err = p.processFeatures(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Features) processFeatures(event *es_models.Event) (err error) {
|
||||
features := new(model.FeaturesView)
|
||||
switch string(event.Type) {
|
||||
case string(org_es_model.OrgAdded):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
case string(iam_repo.FeaturesSetEventType):
|
||||
defaultFeatures, err := p.view.AllDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, features := range defaultFeatures {
|
||||
err = features.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return p.view.PutFeaturesList(defaultFeatures, event)
|
||||
case string(org_repo.FeaturesSetEventType):
|
||||
features, err = p.view.FeaturesByAggregateID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = features.AppendEvent(event)
|
||||
case string(org_repo.FeaturesRemovedEventType):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
default:
|
||||
return p.view.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutFeatures(features, event)
|
||||
}
|
||||
|
||||
func (p *Features) OnError(event *es_models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-Wj8sf", "id", event.AggregateID).WithError(err).Warn("something went wrong in login features handler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestFeaturesFailedEvent, p.view.ProcessedFeaturesFailedEvent, p.view.ProcessedFeaturesSequence, p.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (p *Features) OnSuccess() error {
|
||||
return spooler.HandleSuccess(p.view.UpdateFeaturesSpoolerRunTimestamp)
|
||||
}
|
||||
|
||||
func (p *Features) getDefaultFeatures() (*model.FeaturesView, error) {
|
||||
features, featuresErr := p.view.FeaturesByAggregateID(domain.IAMID)
|
||||
if featuresErr != nil && !caos_errs.IsNotFound(featuresErr) {
|
||||
return nil, featuresErr
|
||||
}
|
||||
if features == nil {
|
||||
features = &model.FeaturesView{}
|
||||
}
|
||||
events, err := p.getIAMEvents(features.Sequence)
|
||||
if err != nil {
|
||||
return features, featuresErr
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return features, nil
|
||||
}
|
||||
}
|
||||
return &featuresCopy, nil
|
||||
}
|
||||
|
||||
func (p *Features) getIAMEvents(sequence uint64) ([]*es_models.Event, error) {
|
||||
query, err := eventsourcing.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.es.FilterEvents(context.Background(), query)
|
||||
}
|
@@ -68,6 +68,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
|
||||
handler{view, bulkLimit, configs.cycleDuration("OrgIAMPolicy"), errorCount, es}),
|
||||
newProjectRole(handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount, es}),
|
||||
newLabelPolicy(handler{view, bulkLimit, configs.cycleDuration("LabelPolicy"), errorCount, es}),
|
||||
newFeatures(handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -42,6 +42,7 @@ type EsRepository struct {
|
||||
eventstore.UserGrantRepo
|
||||
eventstore.OrgRepository
|
||||
eventstore.IAMRepository
|
||||
eventstore.FeaturesRepo
|
||||
}
|
||||
|
||||
func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, authZRepo *authz_repo.EsRepository, esV2 *es2.Eventstore) (*EsRepository, error) {
|
||||
@@ -142,6 +143,10 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
|
||||
IAMID: systemDefaults.IamID,
|
||||
IAMV2QuerySide: queries,
|
||||
},
|
||||
eventstore.FeaturesRepo{
|
||||
Eventstore: es,
|
||||
View: view,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
56
internal/auth/repository/eventsourcing/view/features.go
Normal file
56
internal/auth/repository/eventsourcing/view/features.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/features/repository/view"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "auth.features"
|
||||
)
|
||||
|
||||
func (v *View) AllDefaultFeatures() ([]*model.FeaturesView, error) {
|
||||
return view.GetDefaultFeatures(v.Db, featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) FeaturesByAggregateID(aggregateID string) (*model.FeaturesView, error) {
|
||||
return view.GetFeaturesByAggregateID(v.Db, featuresTable, aggregateID)
|
||||
}
|
||||
|
||||
func (v *View) PutFeatures(features *model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeatures(v.Db, featuresTable, features)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) PutFeaturesList(features []*model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeaturesList(v.Db, featuresTable, features...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesSequence() (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(featuresTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateFeaturesSpoolerRunTimestamp() error {
|
||||
return v.updateSpoolerRunSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(featuresTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
11
internal/auth/repository/features.go
Normal file
11
internal/auth/repository/features.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
)
|
||||
|
||||
type FeaturesRepository interface {
|
||||
GetOrgFeatures(ctx context.Context, id string) (*features_model.FeaturesView, error)
|
||||
}
|
@@ -16,4 +16,5 @@ type Repository interface {
|
||||
UserGrantRepository
|
||||
OrgRepository
|
||||
IAMRepository
|
||||
FeaturesRepository
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
|
||||
@@ -36,7 +37,7 @@ type myUserRepo interface {
|
||||
|
||||
GetMyPasswordless(ctx context.Context) ([]*model.WebAuthNView, error)
|
||||
|
||||
MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error)
|
||||
MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error)
|
||||
|
||||
SearchMyUserMemberships(ctx context.Context, request *model.UserMembershipSearchRequest) (*model.UserMembershipSearchResponse, error)
|
||||
}
|
||||
|
@@ -2,26 +2,26 @@ package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk"
|
||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
usr_view "github.com/caos/zitadel/internal/user/repository/view"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
|
||||
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk"
|
||||
features_view_model "github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
||||
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
usr_view "github.com/caos/zitadel/internal/user/repository/view"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
@@ -111,6 +111,68 @@ func (repo *TokenVerifierRepo) ExistsOrg(ctx context.Context, orgID string) erro
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error {
|
||||
features, err := repo.View.FeaturesByAggregateID(orgID)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
return repo.checkDefaultFeatures(ctx, requiredFeatures...)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkFeatures(features, requiredFeatures...)
|
||||
}
|
||||
|
||||
func checkFeatures(features *features_view_model.FeaturesView, requiredFeatures ...string) error {
|
||||
for _, requiredFeature := range requiredFeatures {
|
||||
if strings.HasPrefix(requiredFeature, domain.FeatureLoginPolicy) {
|
||||
if err := checkLoginPolicyFeatures(features, requiredFeature); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if requiredFeature == domain.FeaturePasswordComplexityPolicy && !features.PasswordComplexityPolicy {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
if requiredFeature == domain.FeatureLabelPolicy && !features.PasswordComplexityPolicy {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkLoginPolicyFeatures(features *features_view_model.FeaturesView, requiredFeature string) error {
|
||||
switch requiredFeature {
|
||||
case domain.FeatureLoginPolicyFactors:
|
||||
if !features.LoginPolicyFactors {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
case domain.FeatureLoginPolicyIDP:
|
||||
if !features.LoginPolicyIDP {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
case domain.FeatureLoginPolicyPasswordless:
|
||||
if !features.LoginPolicyPasswordless {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
case domain.FeatureLoginPolicyRegistration:
|
||||
if !features.LoginPolicyRegistration {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
case domain.FeatureLoginPolicyUsernameLogin:
|
||||
if !features.LoginPolicyUsernameLogin {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
default:
|
||||
if !features.LoginPolicyFactors && !features.LoginPolicyIDP && !features.LoginPolicyPasswordless && !features.LoginPolicyRegistration && !features.LoginPolicyUsernameLogin {
|
||||
return MissingFeatureErr(requiredFeature)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MissingFeatureErr(feature string) error {
|
||||
return caos_errs.ThrowPermissionDeniedf(nil, "AUTH-Dvgsf", "missing feature %v", feature)
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) VerifierClientID(ctx context.Context, appName string) (_ string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
@@ -150,3 +212,36 @@ func (u *TokenVerifierRepo) getIAMByID(ctx context.Context) (*iam_model.IAM, err
|
||||
}
|
||||
return iam_es_model.IAMToModel(iam), nil
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) checkDefaultFeatures(ctx context.Context, requiredFeatures ...string) error {
|
||||
features, viewErr := repo.View.FeaturesByAggregateID(domain.IAMID)
|
||||
if viewErr != nil && !errors.IsNotFound(viewErr) {
|
||||
return viewErr
|
||||
}
|
||||
if errors.IsNotFound(viewErr) {
|
||||
features = new(features_view_model.FeaturesView)
|
||||
}
|
||||
events, esErr := repo.getIAMEvents(ctx, features.Sequence)
|
||||
if errors.IsNotFound(viewErr) && len(events) == 0 {
|
||||
return checkFeatures(features, requiredFeatures...)
|
||||
}
|
||||
if esErr != nil {
|
||||
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
|
||||
return esErr
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return checkFeatures(features, requiredFeatures...)
|
||||
}
|
||||
}
|
||||
return checkFeatures(&featuresCopy, requiredFeatures...)
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) {
|
||||
query, err := iam_view.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.Eventstore.FilterEvents(ctx, query)
|
||||
}
|
||||
|
165
internal/authz/repository/eventsourcing/handler/features.go
Normal file
165
internal/authz/repository/eventsourcing/handler/features.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
iam_repo "github.com/caos/zitadel/internal/repository/iam"
|
||||
org_repo "github.com/caos/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "authz.features"
|
||||
)
|
||||
|
||||
type Features struct {
|
||||
handler
|
||||
subscription *v1.Subscription
|
||||
}
|
||||
|
||||
func newFeatures(handler handler) *Features {
|
||||
h := &Features{
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (p *Features) subscribe() {
|
||||
p.subscription = p.es.Subscribe(p.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range p.subscription.Events {
|
||||
query.ReduceEvent(p, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Features) ViewModel() string {
|
||||
return featuresTable
|
||||
}
|
||||
|
||||
func (p *Features) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{iam_es_model.IAMAggregate, org_es_model.OrgAggregate}
|
||||
}
|
||||
|
||||
func (p *Features) EventQuery() (*es_models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(p.AggregateTypes()...).
|
||||
LatestSequenceFilter(sequence.CurrentSequence), nil
|
||||
}
|
||||
|
||||
func (p *Features) CurrentSequence() (uint64, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (p *Features) Reduce(event *es_models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case org_es_model.OrgAggregate, iam_es_model.IAMAggregate:
|
||||
err = p.processFeatures(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Features) processFeatures(event *es_models.Event) (err error) {
|
||||
features := new(model.FeaturesView)
|
||||
switch string(event.Type) {
|
||||
case string(org_es_model.OrgAdded):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
case string(iam_repo.FeaturesSetEventType):
|
||||
defaultFeatures, err := p.view.AllDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, features := range defaultFeatures {
|
||||
err = features.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return p.view.PutFeaturesList(defaultFeatures, event)
|
||||
case string(org_repo.FeaturesSetEventType):
|
||||
features, err = p.view.FeaturesByAggregateID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = features.AppendEvent(event)
|
||||
case string(org_repo.FeaturesRemovedEventType):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
default:
|
||||
return p.view.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutFeatures(features, event)
|
||||
}
|
||||
|
||||
func (p *Features) OnError(event *es_models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-Wj8sf", "id", event.AggregateID).WithError(err).Warn("something went wrong in login features handler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestFeaturesFailedEvent, p.view.ProcessedFeaturesFailedEvent, p.view.ProcessedFeaturesSequence, p.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (p *Features) OnSuccess() error {
|
||||
return spooler.HandleSuccess(p.view.UpdateFeaturesSpoolerRunTimestamp)
|
||||
}
|
||||
|
||||
func (p *Features) getDefaultFeatures() (*model.FeaturesView, error) {
|
||||
features, featuresErr := p.view.FeaturesByAggregateID(domain.IAMID)
|
||||
if featuresErr != nil && !caos_errs.IsNotFound(featuresErr) {
|
||||
return nil, featuresErr
|
||||
}
|
||||
if features == nil {
|
||||
features = &model.FeaturesView{}
|
||||
}
|
||||
events, err := p.getIAMEvents(features.Sequence)
|
||||
if err != nil {
|
||||
return features, featuresErr
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return features, nil
|
||||
}
|
||||
}
|
||||
return &featuresCopy, nil
|
||||
}
|
||||
|
||||
func (p *Features) getIAMEvents(sequence uint64) ([]*es_models.Event, error) {
|
||||
query, err := eventsourcing.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.es.FilterEvents(context.Background(), query)
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
|
||||
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
@@ -40,6 +41,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
|
||||
handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es}),
|
||||
newOrg(
|
||||
handler{view, bulkLimit, configs.cycleDuration("Org"), errorCount, es}),
|
||||
newFeatures(
|
||||
handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
|
||||
}
|
||||
}
|
||||
|
||||
|
56
internal/authz/repository/eventsourcing/view/features.go
Normal file
56
internal/authz/repository/eventsourcing/view/features.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/features/repository/view"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "authz.features"
|
||||
)
|
||||
|
||||
func (v *View) AllDefaultFeatures() ([]*model.FeaturesView, error) {
|
||||
return view.GetDefaultFeatures(v.Db, featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) FeaturesByAggregateID(aggregateID string) (*model.FeaturesView, error) {
|
||||
return view.GetFeaturesByAggregateID(v.Db, featuresTable, aggregateID)
|
||||
}
|
||||
|
||||
func (v *View) PutFeatures(features *model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeatures(v.Db, featuresTable, features)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) PutFeaturesList(features []*model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeaturesList(v.Db, featuresTable, features...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesSequence() (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(featuresTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateFeaturesSpoolerRunTimestamp() error {
|
||||
return v.updateSpoolerRunSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(featuresTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@@ -3,6 +3,7 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
@@ -49,13 +50,14 @@ type Commands struct {
|
||||
keyAlgorithm crypto.EncryptionAlgorithm
|
||||
privateKeyLifetime time.Duration
|
||||
publicKeyLifetime time.Duration
|
||||
tokenVerifier *authz.TokenVerifier
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Eventstore types.SQLUser
|
||||
}
|
||||
|
||||
func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults, authZConfig authz.Config) (repo *Commands, err error) {
|
||||
func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults, authZConfig authz.Config, authZRepo *authz_repo.EsRepository) (repo *Commands, err error) {
|
||||
repo = &Commands{
|
||||
eventstore: eventstore,
|
||||
idGenerator: id.SonyFlakeGenerator,
|
||||
@@ -119,6 +121,8 @@ func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults
|
||||
return nil, err
|
||||
}
|
||||
repo.keyAlgorithm = keyAlgorithm
|
||||
|
||||
repo.tokenVerifier = authz.Start(authZRepo)
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
|
74
internal/command/features_model.go
Normal file
74
internal/command/features_model.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/repository/features"
|
||||
)
|
||||
|
||||
type FeaturesWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
TierName string
|
||||
TierDescription string
|
||||
State domain.FeaturesState
|
||||
StateDescription string
|
||||
AuditLogRetention time.Duration
|
||||
LoginPolicyFactors bool
|
||||
LoginPolicyIDP bool
|
||||
LoginPolicyPasswordless bool
|
||||
LoginPolicyRegistration bool
|
||||
LoginPolicyUsernameLogin bool
|
||||
PasswordComplexityPolicy bool
|
||||
LabelPolicy bool
|
||||
}
|
||||
|
||||
func (wm *FeaturesWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *features.FeaturesSetEvent:
|
||||
if e.TierName != nil {
|
||||
wm.TierName = *e.TierName
|
||||
}
|
||||
if e.TierDescription != nil {
|
||||
wm.TierDescription = *e.TierDescription
|
||||
}
|
||||
wm.State = domain.FeaturesStateActive
|
||||
if e.State != nil {
|
||||
wm.State = *e.State
|
||||
}
|
||||
if e.StateDescription != nil {
|
||||
wm.StateDescription = *e.StateDescription
|
||||
}
|
||||
if e.AuditLogRetention != nil {
|
||||
wm.AuditLogRetention = *e.AuditLogRetention
|
||||
}
|
||||
if e.LoginPolicyFactors != nil {
|
||||
wm.LoginPolicyFactors = *e.LoginPolicyFactors
|
||||
}
|
||||
if e.LoginPolicyIDP != nil {
|
||||
wm.LoginPolicyIDP = *e.LoginPolicyIDP
|
||||
}
|
||||
if e.LoginPolicyPasswordless != nil {
|
||||
wm.LoginPolicyPasswordless = *e.LoginPolicyPasswordless
|
||||
}
|
||||
if e.LoginPolicyRegistration != nil {
|
||||
wm.LoginPolicyRegistration = *e.LoginPolicyRegistration
|
||||
}
|
||||
if e.LoginPolicyUsernameLogin != nil {
|
||||
wm.LoginPolicyUsernameLogin = *e.LoginPolicyUsernameLogin
|
||||
}
|
||||
if e.PasswordComplexityPolicy != nil {
|
||||
wm.PasswordComplexityPolicy = *e.PasswordComplexityPolicy
|
||||
}
|
||||
if e.LabelPolicy != nil {
|
||||
wm.LabelPolicy = *e.LabelPolicy
|
||||
}
|
||||
case *features.FeaturesRemovedEvent:
|
||||
wm.State = domain.FeaturesStateRemoved
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
@@ -160,3 +160,21 @@ func writeModelToIDPProvider(wm *IdentityProviderWriteModel) *domain.IDPProvider
|
||||
Type: wm.IDPProviderType,
|
||||
}
|
||||
}
|
||||
|
||||
func writeModelToFeatures(wm *FeaturesWriteModel) *domain.Features {
|
||||
return &domain.Features{
|
||||
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
|
||||
TierName: wm.TierName,
|
||||
TierDescription: wm.TierDescription,
|
||||
State: wm.State,
|
||||
StateDescription: wm.StateDescription,
|
||||
AuditLogRetention: wm.AuditLogRetention,
|
||||
LoginPolicyFactors: wm.LoginPolicyFactors,
|
||||
LoginPolicyIDP: wm.LoginPolicyIDP,
|
||||
LoginPolicyPasswordless: wm.LoginPolicyPasswordless,
|
||||
LoginPolicyRegistration: wm.LoginPolicyRegistration,
|
||||
LoginPolicyUsernameLogin: wm.LoginPolicyUsernameLogin,
|
||||
PasswordComplexityPolicy: wm.PasswordComplexityPolicy,
|
||||
LabelPolicy: wm.LabelPolicy,
|
||||
}
|
||||
}
|
||||
|
64
internal/command/iam_features.go
Normal file
64
internal/command/iam_features.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/repository/iam"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func (c *Commands) SetDefaultFeatures(ctx context.Context, features *domain.Features) (*domain.ObjectDetails, error) {
|
||||
existingFeatures := NewIAMFeaturesWriteModel()
|
||||
setEvent, err := c.setDefaultFeatures(ctx, existingFeatures, features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, setEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(existingFeatures, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingFeatures.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAMFeaturesWriteModel, features *domain.Features) (*iam.FeaturesSetEvent, error) {
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setEvent, hasChanged := existingFeatures.NewSetEvent(
|
||||
ctx,
|
||||
IAMAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel),
|
||||
features.TierName,
|
||||
features.TierDescription,
|
||||
features.State,
|
||||
features.StateDescription,
|
||||
features.AuditLogRetention,
|
||||
features.LoginPolicyFactors,
|
||||
features.LoginPolicyIDP,
|
||||
features.LoginPolicyPasswordless,
|
||||
features.LoginPolicyRegistration,
|
||||
features.LoginPolicyUsernameLogin,
|
||||
features.PasswordComplexityPolicy,
|
||||
features.LabelPolicy,
|
||||
)
|
||||
if !hasChanged {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")
|
||||
}
|
||||
return setEvent, nil
|
||||
}
|
||||
|
||||
func (c *Commands) getDefaultFeatures(ctx context.Context) (*domain.Features, error) {
|
||||
existingFeatures := NewIAMFeaturesWriteModel()
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToFeatures(&existingFeatures.FeaturesWriteModel), nil
|
||||
}
|
115
internal/command/iam_features_model.go
Normal file
115
internal/command/iam_features_model.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/repository/features"
|
||||
"github.com/caos/zitadel/internal/repository/iam"
|
||||
)
|
||||
|
||||
type IAMFeaturesWriteModel struct {
|
||||
FeaturesWriteModel
|
||||
}
|
||||
|
||||
func NewIAMFeaturesWriteModel() *IAMFeaturesWriteModel {
|
||||
return &IAMFeaturesWriteModel{
|
||||
FeaturesWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: domain.IAMID,
|
||||
ResourceOwner: domain.IAMID,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *IAMFeaturesWriteModel) AppendEvents(events ...eventstore.EventReader) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *iam.FeaturesSetEvent:
|
||||
wm.FeaturesWriteModel.AppendEvents(&e.FeaturesSetEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *IAMFeaturesWriteModel) IsValid() bool {
|
||||
return wm.AggregateID != ""
|
||||
}
|
||||
|
||||
func (wm *IAMFeaturesWriteModel) Reduce() error {
|
||||
return wm.FeaturesWriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *IAMFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType).
|
||||
AggregateIDs(wm.FeaturesWriteModel.AggregateID).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
EventTypes(iam.FeaturesSetEventType)
|
||||
}
|
||||
|
||||
func (wm *IAMFeaturesWriteModel) NewSetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
tierName, tierDescription string,
|
||||
state domain.FeaturesState,
|
||||
stateDescription string,
|
||||
auditLogRetention time.Duration,
|
||||
loginPolicyFactors,
|
||||
loginPolicyIDP,
|
||||
loginPolicyPasswordless,
|
||||
loginPolicyRegistration,
|
||||
loginPolicyUsernameLogin,
|
||||
passwordComplexityPolicy,
|
||||
labelPolicy bool,
|
||||
) (*iam.FeaturesSetEvent, bool) {
|
||||
|
||||
changes := make([]features.FeaturesChanges, 0)
|
||||
|
||||
if tierName != "" && wm.TierName != tierName {
|
||||
changes = append(changes, features.ChangeTierName(tierName))
|
||||
}
|
||||
if tierDescription != "" && wm.TierDescription != tierDescription {
|
||||
changes = append(changes, features.ChangeTierDescription(tierDescription))
|
||||
}
|
||||
if wm.State != state {
|
||||
changes = append(changes, features.ChangeState(state))
|
||||
}
|
||||
if stateDescription != "" && wm.StateDescription != stateDescription {
|
||||
changes = append(changes, features.ChangeStateDescription(stateDescription))
|
||||
}
|
||||
if auditLogRetention != 0 && wm.AuditLogRetention != auditLogRetention {
|
||||
changes = append(changes, features.ChangeAuditLogRetention(auditLogRetention))
|
||||
}
|
||||
if wm.LoginPolicyFactors != loginPolicyFactors {
|
||||
changes = append(changes, features.ChangeLoginPolicyFactors(loginPolicyFactors))
|
||||
}
|
||||
if wm.LoginPolicyIDP != loginPolicyIDP {
|
||||
changes = append(changes, features.ChangeLoginPolicyIDP(loginPolicyIDP))
|
||||
}
|
||||
if wm.LoginPolicyPasswordless != loginPolicyPasswordless {
|
||||
changes = append(changes, features.ChangeLoginPolicyPasswordless(loginPolicyPasswordless))
|
||||
}
|
||||
if wm.LoginPolicyRegistration != loginPolicyRegistration {
|
||||
changes = append(changes, features.ChangeLoginPolicyRegistration(loginPolicyRegistration))
|
||||
}
|
||||
if wm.LoginPolicyUsernameLogin != loginPolicyUsernameLogin {
|
||||
changes = append(changes, features.ChangeLoginPolicyUsernameLogin(loginPolicyUsernameLogin))
|
||||
}
|
||||
if wm.PasswordComplexityPolicy != passwordComplexityPolicy {
|
||||
changes = append(changes, features.ChangePasswordComplexityPolicy(passwordComplexityPolicy))
|
||||
}
|
||||
if wm.LabelPolicy != labelPolicy {
|
||||
changes = append(changes, features.ChangeLabelPolicy(labelPolicy))
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
changedEvent, err := iam.NewFeaturesSetEvent(ctx, aggregate, changes)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return changedEvent, true
|
||||
}
|
@@ -221,7 +221,7 @@ func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multi
|
||||
return domain.MultiFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "IAM-5m9fs", "Errors.IAM.LoginPolicy.MFA.Unspecified")
|
||||
}
|
||||
multiFactorModel := NewIAMMultiFactorWriteModel(multiFactor)
|
||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
|
||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
|
||||
event, err := c.addMultiFactorToDefaultLoginPolicy(ctx, iamAgg, multiFactorModel, multiFactor)
|
||||
if err != nil {
|
||||
return domain.MultiFactorTypeUnspecified, nil, err
|
||||
@@ -235,7 +235,7 @@ func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multi
|
||||
if err != nil {
|
||||
return domain.MultiFactorTypeUnspecified, nil, err
|
||||
}
|
||||
return multiFactorModel.MultiFactoryWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
|
||||
return multiFactorModel.MultiFactorWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) addMultiFactorToDefaultLoginPolicy(ctx context.Context, iamAgg *eventstore.Aggregate, multiFactorModel *IAMMultiFactorWriteModel, multiFactor domain.MultiFactorType) (eventstore.EventPusher, error) {
|
||||
@@ -262,7 +262,7 @@ func (c *Commands) RemoveMultiFactorFromDefaultLoginPolicy(ctx context.Context,
|
||||
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "IAM-3M9df", "Errors.IAM.LoginPolicy.MFA.NotExisting")
|
||||
}
|
||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
|
||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, iam_repo.NewLoginPolicyMultiFactorRemovedEvent(ctx, iamAgg, multiFactor))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@@ -51,12 +51,12 @@ func (wm *IAMSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
}
|
||||
|
||||
type IAMMultiFactorWriteModel struct {
|
||||
MultiFactoryWriteModel
|
||||
MultiFactorWriteModel
|
||||
}
|
||||
|
||||
func NewIAMMultiFactorWriteModel(factorType domain.MultiFactorType) *IAMMultiFactorWriteModel {
|
||||
return &IAMMultiFactorWriteModel{
|
||||
MultiFactoryWriteModel{
|
||||
MultiFactorWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: domain.IAMID,
|
||||
ResourceOwner: domain.IAMID,
|
||||
@@ -82,7 +82,7 @@ func (wm *IAMMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
|
||||
}
|
||||
|
||||
func (wm *IAMMultiFactorWriteModel) Reduce() error {
|
||||
return wm.MultiFactoryWriteModel.Reduce()
|
||||
return wm.MultiFactorWriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *IAMMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
|
@@ -2,7 +2,14 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository/mock"
|
||||
@@ -12,9 +19,6 @@ import (
|
||||
proj_repo "github.com/caos/zitadel/internal/repository/project"
|
||||
usr_repo "github.com/caos/zitadel/internal/repository/user"
|
||||
"github.com/caos/zitadel/internal/repository/usergrant"
|
||||
"github.com/golang/mock/gomock"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type expect func(mockRepository *mock.MockRepository)
|
||||
@@ -172,3 +176,48 @@ func GetMockSecretGenerator(t *testing.T) crypto.Generator {
|
||||
|
||||
return generator
|
||||
}
|
||||
|
||||
func GetMockVerifier(t *testing.T, features ...string) *authz.TokenVerifier {
|
||||
return authz.Start(&testVerifier{
|
||||
features: features,
|
||||
})
|
||||
}
|
||||
|
||||
type testVerifier struct {
|
||||
features []string
|
||||
}
|
||||
|
||||
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, string, string, error) {
|
||||
return "userID", "agentID", "de", "orgID", nil
|
||||
}
|
||||
func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, error) {
|
||||
return "clientID", nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error {
|
||||
for _, feature := range requiredFeatures {
|
||||
hasFeature := false
|
||||
for _, f := range v.features {
|
||||
if f == feature {
|
||||
hasFeature = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasFeature {
|
||||
return errors.ThrowPermissionDenied(nil, "id", "missing feature")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
67
internal/command/org_features.go
Normal file
67
internal/command/org_features.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, features *domain.Features) (*domain.ObjectDetails, error) {
|
||||
existingFeatures := NewOrgFeaturesWriteModel(resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setEvent, hasChanged := existingFeatures.NewSetEvent(
|
||||
ctx,
|
||||
OrgAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel),
|
||||
features.TierName,
|
||||
features.TierDescription,
|
||||
features.State,
|
||||
features.StateDescription,
|
||||
features.AuditLogRetention,
|
||||
features.LoginPolicyFactors,
|
||||
features.LoginPolicyIDP,
|
||||
features.LoginPolicyPasswordless,
|
||||
features.LoginPolicyRegistration,
|
||||
features.LoginPolicyUsernameLogin,
|
||||
features.PasswordComplexityPolicy,
|
||||
features.LabelPolicy,
|
||||
)
|
||||
if !hasChanged {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, setEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(existingFeatures, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingFeatures.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveOrgFeatures(ctx context.Context, orgID string) (*domain.ObjectDetails, error) {
|
||||
existingFeatures := NewOrgFeaturesWriteModel(orgID)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingFeatures.State == domain.FeaturesStateUnspecified || existingFeatures.State == domain.FeaturesStateRemoved {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "Features-Bg32G", "Errors.Features.NotFound")
|
||||
}
|
||||
|
||||
orgAgg := OrgAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel)
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewFeaturesRemovedEvent(ctx, orgAgg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(existingFeatures, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingFeatures.WriteModel), nil
|
||||
}
|
121
internal/command/org_features_model.go
Normal file
121
internal/command/org_features_model.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/repository/features"
|
||||
"github.com/caos/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
type OrgFeaturesWriteModel struct {
|
||||
FeaturesWriteModel
|
||||
}
|
||||
|
||||
func NewOrgFeaturesWriteModel(orgID string) *OrgFeaturesWriteModel {
|
||||
return &OrgFeaturesWriteModel{
|
||||
FeaturesWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: orgID,
|
||||
ResourceOwner: orgID,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *OrgFeaturesWriteModel) AppendEvents(events ...eventstore.EventReader) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *org.FeaturesSetEvent:
|
||||
wm.FeaturesWriteModel.AppendEvents(&e.FeaturesSetEvent)
|
||||
case *org.FeaturesRemovedEvent:
|
||||
wm.FeaturesWriteModel.AppendEvents(&e.FeaturesRemovedEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *OrgFeaturesWriteModel) IsValid() bool {
|
||||
return wm.AggregateID != ""
|
||||
}
|
||||
|
||||
func (wm *OrgFeaturesWriteModel) Reduce() error {
|
||||
return wm.FeaturesWriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *OrgFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType).
|
||||
AggregateIDs(wm.FeaturesWriteModel.AggregateID).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
EventTypes(
|
||||
org.FeaturesSetEventType,
|
||||
org.FeaturesRemovedEventType,
|
||||
)
|
||||
}
|
||||
|
||||
func (wm *OrgFeaturesWriteModel) NewSetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
tierName,
|
||||
tierDescription string,
|
||||
state domain.FeaturesState,
|
||||
stateDescription string,
|
||||
auditLogRetention time.Duration,
|
||||
loginPolicyFactors,
|
||||
loginPolicyIDP,
|
||||
loginPolicyPasswordless,
|
||||
loginPolicyRegistration,
|
||||
loginPolicyUsernameLogin,
|
||||
passwordComplexityPolicy,
|
||||
labelPolicy bool,
|
||||
) (*org.FeaturesSetEvent, bool) {
|
||||
|
||||
changes := make([]features.FeaturesChanges, 0)
|
||||
|
||||
if tierName != "" && wm.TierName != tierName {
|
||||
changes = append(changes, features.ChangeTierName(tierName))
|
||||
}
|
||||
if tierDescription != "" && wm.TierDescription != tierDescription {
|
||||
changes = append(changes, features.ChangeTierDescription(tierDescription))
|
||||
}
|
||||
if wm.State != state {
|
||||
changes = append(changes, features.ChangeState(state))
|
||||
}
|
||||
if stateDescription != "" && wm.StateDescription != stateDescription {
|
||||
changes = append(changes, features.ChangeStateDescription(stateDescription))
|
||||
}
|
||||
if auditLogRetention != 0 && wm.AuditLogRetention != auditLogRetention {
|
||||
changes = append(changes, features.ChangeAuditLogRetention(auditLogRetention))
|
||||
}
|
||||
if wm.LoginPolicyFactors != loginPolicyFactors {
|
||||
changes = append(changes, features.ChangeLoginPolicyFactors(loginPolicyFactors))
|
||||
}
|
||||
if wm.LoginPolicyIDP != loginPolicyIDP {
|
||||
changes = append(changes, features.ChangeLoginPolicyIDP(loginPolicyIDP))
|
||||
}
|
||||
if wm.LoginPolicyPasswordless != loginPolicyPasswordless {
|
||||
changes = append(changes, features.ChangeLoginPolicyPasswordless(loginPolicyPasswordless))
|
||||
}
|
||||
if wm.LoginPolicyRegistration != loginPolicyRegistration {
|
||||
changes = append(changes, features.ChangeLoginPolicyRegistration(loginPolicyRegistration))
|
||||
}
|
||||
if wm.LoginPolicyUsernameLogin != loginPolicyUsernameLogin {
|
||||
changes = append(changes, features.ChangeLoginPolicyUsernameLogin(loginPolicyUsernameLogin))
|
||||
}
|
||||
if wm.PasswordComplexityPolicy != passwordComplexityPolicy {
|
||||
changes = append(changes, features.ChangePasswordComplexityPolicy(passwordComplexityPolicy))
|
||||
}
|
||||
if wm.LabelPolicy != labelPolicy {
|
||||
changes = append(changes, features.ChangeLabelPolicy(labelPolicy))
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
changedEvent, err := org.NewFeaturesSetEvent(ctx, aggregate, changes)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return changedEvent, true
|
||||
}
|
@@ -2,7 +2,11 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
@@ -22,6 +26,11 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
|
||||
return nil, caos_errs.ThrowAlreadyExists(nil, "Org-Dgfb2", "Errors.Org.LoginPolicy.AlreadyExists")
|
||||
}
|
||||
|
||||
err = c.checkLoginPolicyAllowed(ctx, resourceOwner, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel)
|
||||
pushedEvents, err := c.eventstore.PushEvents(
|
||||
ctx,
|
||||
@@ -43,6 +52,15 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
|
||||
return writeModelToLoginPolicy(&addedPolicy.LoginPolicyWriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) orgLoginPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLoginPolicyWriteModel, error) {
|
||||
policyWriteModel := NewOrgLoginPolicyWriteModel(orgID)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, policyWriteModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policyWriteModel, nil
|
||||
}
|
||||
|
||||
func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) {
|
||||
if resourceOwner == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing")
|
||||
@@ -55,6 +73,12 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
|
||||
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound")
|
||||
}
|
||||
|
||||
err = c.checkLoginPolicyAllowed(ctx, resourceOwner, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel)
|
||||
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType)
|
||||
if !hasChanged {
|
||||
@@ -72,6 +96,30 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
|
||||
return writeModelToLoginPolicy(&existingPolicy.LoginPolicyWriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) checkLoginPolicyAllowed(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) error {
|
||||
defaultPolicy, err := c.getDefaultLoginPolicy(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requiredFeatures := make([]string, 0)
|
||||
if defaultPolicy.ForceMFA != policy.ForceMFA || !reflect.DeepEqual(defaultPolicy.MultiFactors, policy.MultiFactors) || !reflect.DeepEqual(defaultPolicy.SecondFactors, policy.SecondFactors) {
|
||||
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyFactors)
|
||||
}
|
||||
if defaultPolicy.AllowExternalIDP != policy.AllowExternalIDP || !reflect.DeepEqual(defaultPolicy.IDPProviders, policy.IDPProviders) {
|
||||
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyIDP)
|
||||
}
|
||||
if defaultPolicy.AllowRegister != policy.AllowRegister {
|
||||
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyRegistration)
|
||||
}
|
||||
if defaultPolicy.PasswordlessType != policy.PasswordlessType {
|
||||
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyPasswordless)
|
||||
}
|
||||
if defaultPolicy.AllowUsernamePassword != policy.AllowUsernamePassword {
|
||||
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyUsernameLogin)
|
||||
}
|
||||
return authz.CheckOrgFeatures(ctx, c.tokenVerifier, resourceOwner, requiredFeatures...)
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) {
|
||||
if orgID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-55Mg9", "Errors.ResourceOwnerMissing")
|
||||
@@ -267,7 +315,7 @@ func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor
|
||||
if err != nil {
|
||||
return domain.MultiFactorTypeUnspecified, nil, err
|
||||
}
|
||||
return multiFactorModel.MultiFactoryWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
|
||||
return multiFactorModel.MultiFactorWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType, orgID string) (*domain.ObjectDetails, error) {
|
||||
@@ -285,7 +333,7 @@ func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFa
|
||||
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LoginPolicy.MFA.NotExisting")
|
||||
}
|
||||
orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
|
||||
orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
|
||||
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicyMultiFactorRemovedEvent(ctx, orgAgg, multiFactor))
|
||||
if err != nil {
|
||||
|
@@ -51,12 +51,12 @@ func (wm *OrgSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
}
|
||||
|
||||
type OrgMultiFactorWriteModel struct {
|
||||
MultiFactoryWriteModel
|
||||
MultiFactorWriteModel
|
||||
}
|
||||
|
||||
func NewOrgMultiFactorWriteModel(orgID string, factorType domain.MultiFactorType) *OrgMultiFactorWriteModel {
|
||||
return &OrgMultiFactorWriteModel{
|
||||
MultiFactoryWriteModel{
|
||||
MultiFactorWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: orgID,
|
||||
ResourceOwner: orgID,
|
||||
@@ -82,7 +82,7 @@ func (wm *OrgMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
|
||||
}
|
||||
|
||||
func (wm *OrgMultiFactorWriteModel) Reduce() error {
|
||||
return wm.MultiFactoryWriteModel.Reduce()
|
||||
return wm.MultiFactorWriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *OrgMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
|
@@ -6,11 +6,13 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/repository/iam"
|
||||
"github.com/caos/zitadel/internal/repository/org"
|
||||
"github.com/caos/zitadel/internal/repository/policy"
|
||||
"github.com/caos/zitadel/internal/repository/user"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
tokenVerifier *authz.TokenVerifier
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -88,12 +91,60 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
||||
err: caos_errs.IsErrorAlreadyExists,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "loginpolicy not allowed, permission denied error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
iam.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&iam.NewAggregate().Aggregate,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
domain.PasswordlessTypeAllowed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: GetMockVerifier(t),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
policy: &domain.LoginPolicy{
|
||||
AllowRegister: true,
|
||||
AllowUsernamePassword: true,
|
||||
AllowExternalIDP: true,
|
||||
ForceMFA: true,
|
||||
PasswordlessType: domain.PasswordlessTypeAllowed,
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsPermissionDenied,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add policy,ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
iam.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&iam.NewAggregate().Aggregate,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
domain.PasswordlessTypeAllowed,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
@@ -109,6 +160,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -140,6 +192,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
tokenVerifier: tt.fields.tokenVerifier,
|
||||
}
|
||||
got, err := r.AddLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
|
||||
if tt.res.err == nil {
|
||||
@@ -158,6 +211,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
||||
func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
tokenVerifier *authz.TokenVerifier
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -218,6 +272,53 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
||||
err: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not allowed, permission denied error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1", "org1").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
domain.PasswordlessTypeAllowed,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
iam.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&iam.NewAggregate().Aggregate,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
domain.PasswordlessTypeAllowed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: GetMockVerifier(t),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
policy: &domain.LoginPolicy{
|
||||
AllowRegister: true,
|
||||
AllowUsernamePassword: true,
|
||||
AllowExternalIDP: true,
|
||||
ForceMFA: true,
|
||||
PasswordlessType: domain.PasswordlessTypeAllowed,
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsPermissionDenied,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes, precondition error",
|
||||
fields: fields{
|
||||
@@ -235,7 +336,20 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
iam.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&iam.NewAggregate().Aggregate,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
domain.PasswordlessTypeAllowed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -269,6 +383,18 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
iam.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&iam.NewAggregate().Aggregate,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
domain.PasswordlessTypeNotAllowed,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
@@ -277,6 +403,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -308,6 +435,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
tokenVerifier: tt.fields.tokenVerifier,
|
||||
}
|
||||
got, err := r.ChangeLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
|
||||
if tt.res.err == nil {
|
||||
|
@@ -26,13 +26,13 @@ func (wm *SecondFactorWriteModel) Reduce() error {
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
type MultiFactoryWriteModel struct {
|
||||
type MultiFactorWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
MFAType domain.MultiFactorType
|
||||
State domain.FactorState
|
||||
}
|
||||
|
||||
func (wm *MultiFactoryWriteModel) Reduce() error {
|
||||
func (wm *MultiFactorWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *policy.MultiFactorAddedEvent:
|
||||
|
52
internal/command/setup_step12.go
Normal file
52
internal/command/setup_step12.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
type Step12 struct {
|
||||
TierName string
|
||||
TierDescription string
|
||||
AuditLogRetention types.Duration
|
||||
LoginPolicyFactors bool
|
||||
LoginPolicyIDP bool
|
||||
LoginPolicyPasswordless bool
|
||||
LoginPolicyRegistration bool
|
||||
LoginPolicyUsernameLogin bool
|
||||
PasswordComplexityPolicy bool
|
||||
}
|
||||
|
||||
func (s *Step12) Step() domain.Step {
|
||||
return domain.Step12
|
||||
}
|
||||
|
||||
func (s *Step12) execute(ctx context.Context, commandSide *Commands) error {
|
||||
return commandSide.SetupStep12(ctx, s)
|
||||
}
|
||||
|
||||
func (c *Commands) SetupStep12(ctx context.Context, step *Step12) error {
|
||||
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
|
||||
featuresWriteModel := NewIAMFeaturesWriteModel()
|
||||
featuresEvent, err := c.setDefaultFeatures(ctx, featuresWriteModel, &domain.Features{
|
||||
TierName: step.TierName,
|
||||
TierDescription: step.TierDescription,
|
||||
State: domain.FeaturesStateActive,
|
||||
AuditLogRetention: step.AuditLogRetention.Duration,
|
||||
LoginPolicyFactors: step.LoginPolicyFactors,
|
||||
LoginPolicyIDP: step.LoginPolicyIDP,
|
||||
LoginPolicyPasswordless: step.LoginPolicyPasswordless,
|
||||
LoginPolicyRegistration: step.LoginPolicyRegistration,
|
||||
LoginPolicyUsernameLogin: step.LoginPolicyUsernameLogin,
|
||||
PasswordComplexityPolicy: step.PasswordComplexityPolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []eventstore.EventPusher{featuresEvent}, nil
|
||||
}
|
||||
return c.setup(ctx, step, fn)
|
||||
}
|
@@ -23,7 +23,7 @@ func (s *Step9) execute(ctx context.Context, commandSide *Commands) error {
|
||||
func (c *Commands) SetupStep9(ctx context.Context, step *Step9) error {
|
||||
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
|
||||
multiFactorModel := NewIAMMultiFactorWriteModel(domain.MultiFactorTypeU2FWithPIN)
|
||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
|
||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
|
||||
if !step.Passwordless {
|
||||
return []eventstore.EventPusher{}, nil
|
||||
}
|
||||
|
54
internal/domain/features.go
Normal file
54
internal/domain/features.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
const (
|
||||
FeatureLoginPolicy = "login_policy"
|
||||
FeatureLoginPolicyFactors = FeatureLoginPolicy + ".factors"
|
||||
FeatureLoginPolicyIDP = FeatureLoginPolicy + ".idp"
|
||||
FeatureLoginPolicyPasswordless = FeatureLoginPolicy + ".passwordless"
|
||||
FeatureLoginPolicyRegistration = FeatureLoginPolicy + ".registration"
|
||||
FeatureLoginPolicyUsernameLogin = FeatureLoginPolicy + ".username_login"
|
||||
FeaturePasswordComplexityPolicy = "password_complexity_policy"
|
||||
FeatureLabelPolicy = "label_policy"
|
||||
)
|
||||
|
||||
type Features struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
TierName string
|
||||
TierDescription string
|
||||
State FeaturesState
|
||||
StateDescription string
|
||||
IsDefault bool
|
||||
|
||||
AuditLogRetention time.Duration
|
||||
LoginPolicyFactors bool
|
||||
LoginPolicyIDP bool
|
||||
LoginPolicyPasswordless bool
|
||||
LoginPolicyRegistration bool
|
||||
LoginPolicyUsernameLogin bool
|
||||
PasswordComplexityPolicy bool
|
||||
LabelPolicy bool
|
||||
}
|
||||
|
||||
type FeaturesState int32
|
||||
|
||||
const (
|
||||
FeaturesStateUnspecified FeaturesState = iota
|
||||
FeaturesStateActive
|
||||
FeaturesStateActionRequired
|
||||
FeaturesStateCanceled
|
||||
FeaturesStateGrandfathered
|
||||
FeaturesStateRemoved
|
||||
|
||||
featuresStateCount
|
||||
)
|
||||
|
||||
func (f FeaturesState) Valid() bool {
|
||||
return f >= 0 && f < featuresStateCount
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@@ -14,6 +14,7 @@ const (
|
||||
Step9
|
||||
Step10
|
||||
Step11
|
||||
Step12
|
||||
//StepCount marks the the length of possible steps (StepCount-1 == last possible step)
|
||||
StepCount
|
||||
)
|
||||
|
@@ -181,6 +181,8 @@ func getField(field es_models.Field) string {
|
||||
return "editor_user"
|
||||
case es_models.Field_EventType:
|
||||
return "event_type"
|
||||
case es_models.Field_CreationDate:
|
||||
return "creation_date"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@@ -10,4 +10,5 @@ const (
|
||||
Field_EditorService
|
||||
Field_EditorUser
|
||||
Field_EventType
|
||||
Field_CreationDate
|
||||
)
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
@@ -15,6 +17,7 @@ type SearchQueryFactory struct {
|
||||
sequenceTo uint64
|
||||
eventTypes []EventType
|
||||
resourceOwner string
|
||||
creationDate time.Time
|
||||
}
|
||||
|
||||
type searchQuery struct {
|
||||
@@ -63,7 +66,8 @@ func FactoryFromSearchQuery(query *SearchQuery) *SearchQueryFactory {
|
||||
factory = factory.EventTypes(filter.value.([]EventType)...)
|
||||
case Field_EditorService, Field_EditorUser:
|
||||
logging.Log("MODEL-Mr0VN").WithField("value", filter.value).Panic("field not converted to factory")
|
||||
|
||||
case Field_CreationDate:
|
||||
factory = factory.CreationDateNewer(filter.value.(time.Time))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +120,11 @@ func (factory *SearchQueryFactory) ResourceOwner(resourceOwner string) *SearchQu
|
||||
return factory
|
||||
}
|
||||
|
||||
func (factory *SearchQueryFactory) CreationDateNewer(time time.Time) *SearchQueryFactory {
|
||||
factory.creationDate = time
|
||||
return factory
|
||||
}
|
||||
|
||||
func (factory *SearchQueryFactory) OrderDesc() *SearchQueryFactory {
|
||||
factory.desc = true
|
||||
return factory
|
||||
@@ -142,6 +151,7 @@ func (factory *SearchQueryFactory) Build() (*searchQuery, error) {
|
||||
factory.sequenceToFilter,
|
||||
factory.eventTypeFilter,
|
||||
factory.resourceOwnerFilter,
|
||||
factory.creationDateNewerFilter,
|
||||
} {
|
||||
if filter := f(); filter != nil {
|
||||
filters = append(filters, filter)
|
||||
@@ -211,3 +221,10 @@ func (factory *SearchQueryFactory) resourceOwnerFilter() *Filter {
|
||||
}
|
||||
return NewFilter(Field_ResourceOwner, factory.resourceOwner, Operation_Equals)
|
||||
}
|
||||
|
||||
func (factory *SearchQueryFactory) creationDateNewerFilter() *Filter {
|
||||
if factory.creationDate.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return NewFilter(Field_CreationDate, factory.creationDate, Operation_Greater)
|
||||
}
|
||||
|
@@ -1,6 +1,10 @@
|
||||
package models
|
||||
|
||||
import "github.com/caos/zitadel/internal/errors"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
//SearchQuery is deprecated. Use SearchQueryFactory
|
||||
type SearchQuery struct {
|
||||
@@ -68,6 +72,10 @@ func (q *SearchQuery) ResourceOwnerFilter(resourceOwner string) *SearchQuery {
|
||||
return q.setFilter(NewFilter(Field_ResourceOwner, resourceOwner, Operation_Equals))
|
||||
}
|
||||
|
||||
func (q *SearchQuery) CreationDateNewerFilter(time time.Time) *SearchQuery {
|
||||
return q.setFilter(NewFilter(Field_CreationDate, time, Operation_Greater))
|
||||
}
|
||||
|
||||
func (q *SearchQuery) setFilter(filter *Filter) *SearchQuery {
|
||||
for i, f := range q.Filters {
|
||||
if f.field == filter.field && f.field != Field_LatestSequence {
|
||||
|
85
internal/features/model/features_view.go
Normal file
85
internal/features/model/features_view.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
type FeaturesView struct {
|
||||
AggregateID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
Sequence uint64
|
||||
Default bool
|
||||
|
||||
TierName string
|
||||
TierDescription string
|
||||
State domain.FeaturesState
|
||||
StateDescription string
|
||||
AuditLogRetention time.Duration
|
||||
LoginPolicyFactors bool
|
||||
LoginPolicyIDP bool
|
||||
LoginPolicyPasswordless bool
|
||||
LoginPolicyRegistration bool
|
||||
LoginPolicyUsernameLogin bool
|
||||
PasswordComplexityPolicy bool
|
||||
LabelPolicy bool
|
||||
}
|
||||
|
||||
func (f *FeaturesView) FeatureList() []string {
|
||||
list := make([]string, 0, 6)
|
||||
if f.LoginPolicyFactors {
|
||||
list = append(list, domain.FeatureLoginPolicyFactors)
|
||||
}
|
||||
if f.LoginPolicyIDP {
|
||||
list = append(list, domain.FeatureLoginPolicyIDP)
|
||||
}
|
||||
if f.LoginPolicyPasswordless {
|
||||
list = append(list, domain.FeatureLoginPolicyPasswordless)
|
||||
}
|
||||
if f.LoginPolicyRegistration {
|
||||
list = append(list, domain.FeatureLoginPolicyRegistration)
|
||||
}
|
||||
if f.LoginPolicyUsernameLogin {
|
||||
list = append(list, domain.FeatureLoginPolicyUsernameLogin)
|
||||
}
|
||||
if f.PasswordComplexityPolicy {
|
||||
list = append(list, domain.FeaturePasswordComplexityPolicy)
|
||||
}
|
||||
if f.LabelPolicy {
|
||||
list = append(list, domain.FeatureLabelPolicy)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
type FeaturesSearchRequest struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
SortingColumn FeaturesSearchKey
|
||||
Asc bool
|
||||
Queries []*FeaturesSearchQuery
|
||||
}
|
||||
|
||||
type FeaturesSearchKey int32
|
||||
|
||||
const (
|
||||
FeaturesSearchKeyUnspecified FeaturesSearchKey = iota
|
||||
FeaturesSearchKeyAggregateID
|
||||
FeaturesSearchKeyDefault
|
||||
)
|
||||
|
||||
type FeaturesSearchQuery struct {
|
||||
Key FeaturesSearchKey
|
||||
Method domain.SearchMethod
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type FeaturesSearchResult struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
TotalResult uint64
|
||||
Result []*FeaturesView
|
||||
Sequence uint64
|
||||
Timestamp time.Time
|
||||
}
|
48
internal/features/repository/view/features_view.go
Normal file
48
internal/features/repository/view/features_view.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/features/model"
|
||||
view_model "github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
func GetDefaultFeatures(db *gorm.DB, table string) ([]*view_model.FeaturesView, error) {
|
||||
features := make([]*view_model.FeaturesView, 0)
|
||||
queries := []*model.FeaturesSearchQuery{
|
||||
{Key: model.FeaturesSearchKeyDefault, Value: true, Method: domain.SearchMethodEquals},
|
||||
}
|
||||
query := repository.PrepareSearchQuery(table, view_model.FeaturesSearchRequest{Queries: queries})
|
||||
_, err := query(db, &features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return features, nil
|
||||
}
|
||||
|
||||
func GetFeaturesByAggregateID(db *gorm.DB, table, aggregateID string) (*view_model.FeaturesView, error) {
|
||||
features := new(view_model.FeaturesView)
|
||||
query := repository.PrepareGetByKey(table, view_model.FeaturesSearchKey(model.FeaturesSearchKeyAggregateID), aggregateID)
|
||||
err := query(db, features)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "VIEW-Dbf3h", "Errors.Features.NotFound")
|
||||
}
|
||||
return features, err
|
||||
}
|
||||
|
||||
func PutFeatures(db *gorm.DB, table string, features *view_model.FeaturesView) error {
|
||||
save := repository.PrepareSave(table)
|
||||
return save(db, features)
|
||||
}
|
||||
|
||||
func PutFeaturesList(db *gorm.DB, table string, featuresList ...*view_model.FeaturesView) error {
|
||||
save := repository.PrepareBulkSave(table)
|
||||
f := make([]interface{}, len(featuresList))
|
||||
for i, features := range featuresList {
|
||||
f[i] = features
|
||||
}
|
||||
return save(db, f...)
|
||||
}
|
95
internal/features/repository/view/model/features.go
Normal file
95
internal/features/repository/view/model/features.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
iam_repo "github.com/caos/zitadel/internal/repository/iam"
|
||||
org_repo "github.com/caos/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
const (
|
||||
FeaturesKeyAggregateID = "aggregate_id"
|
||||
FeaturesKeyDefault = "default_features"
|
||||
)
|
||||
|
||||
type FeaturesView struct {
|
||||
AggregateID string `json:"-" gorm:"column:aggregate_id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
Default bool `json:"-" gorm:"column:default_features"`
|
||||
|
||||
TierName string `json:"tierName" gorm:"column:tier_name"`
|
||||
TierDescription string `json:"tierDescription" gorm:"column:tier_description"`
|
||||
State int32 `json:"state" gorm:"column:state"`
|
||||
StateDescription string `json:"stateDescription" gorm:"column:state_description"`
|
||||
AuditLogRetention time.Duration `json:"auditLogRetention" gorm:"column:audit_log_retention"`
|
||||
LoginPolicyFactors bool `json:"loginPolicyFactors" gorm:"column:login_policy_factors"`
|
||||
LoginPolicyIDP bool `json:"loginPolicyIDP" gorm:"column:login_policy_idp"`
|
||||
LoginPolicyPasswordless bool `json:"loginPolicyPasswordless" gorm:"column:login_policy_passwordless"`
|
||||
LoginPolicyRegistration bool `json:"loginPolicyRegistration" gorm:"column:login_policy_registration"`
|
||||
LoginPolicyUsernameLogin bool `json:"loginPolicyUsernameLogin" gorm:"column:login_policy_username_login"`
|
||||
PasswordComplexityPolicy bool `json:"passwordComplexityPolicy" gorm:"column:password_complexity_policy"`
|
||||
LabelPolicy bool `json:"labelPolicy" gorm:"column:label_policy"`
|
||||
}
|
||||
|
||||
func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView {
|
||||
return &features_model.FeaturesView{
|
||||
AggregateID: features.AggregateID,
|
||||
CreationDate: features.CreationDate,
|
||||
ChangeDate: features.ChangeDate,
|
||||
Sequence: features.Sequence,
|
||||
Default: features.Default,
|
||||
TierName: features.TierName,
|
||||
TierDescription: features.TierDescription,
|
||||
State: domain.FeaturesState(features.State),
|
||||
StateDescription: features.StateDescription,
|
||||
AuditLogRetention: features.AuditLogRetention,
|
||||
LoginPolicyFactors: features.LoginPolicyFactors,
|
||||
LoginPolicyIDP: features.LoginPolicyIDP,
|
||||
LoginPolicyPasswordless: features.LoginPolicyPasswordless,
|
||||
LoginPolicyRegistration: features.LoginPolicyRegistration,
|
||||
LoginPolicyUsernameLogin: features.LoginPolicyUsernameLogin,
|
||||
PasswordComplexityPolicy: features.PasswordComplexityPolicy,
|
||||
LabelPolicy: features.LabelPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FeaturesView) AppendEvent(event *models.Event) (err error) {
|
||||
f.Sequence = event.Sequence
|
||||
f.ChangeDate = event.CreationDate
|
||||
switch string(event.Type) {
|
||||
case string(iam_repo.FeaturesSetEventType):
|
||||
f.SetRootData(event)
|
||||
f.CreationDate = event.CreationDate
|
||||
f.Default = true
|
||||
err = f.SetData(event)
|
||||
case string(org_repo.FeaturesSetEventType):
|
||||
f.SetRootData(event)
|
||||
f.CreationDate = event.CreationDate
|
||||
err = f.SetData(event)
|
||||
f.Default = false
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *FeaturesView) SetRootData(event *models.Event) {
|
||||
if f.AggregateID == "" {
|
||||
f.AggregateID = event.AggregateID
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FeaturesView) SetData(event *models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, f); err != nil {
|
||||
logging.Log("EVEN-DVsf2").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-Bfg31", "Could not unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
61
internal/features/repository/view/model/features_query.go
Normal file
61
internal/features/repository/view/model/features_query.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/features/model"
|
||||
"github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
type FeaturesSearchRequest model.FeaturesSearchRequest
|
||||
type FeaturesSearchQuery model.FeaturesSearchQuery
|
||||
type FeaturesSearchKey model.FeaturesSearchKey
|
||||
|
||||
func (req FeaturesSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req FeaturesSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req FeaturesSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||
if req.SortingColumn == model.FeaturesSearchKeyUnspecified {
|
||||
return nil
|
||||
}
|
||||
return FeaturesSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req FeaturesSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req FeaturesSearchRequest) GetQueries() []repository.SearchQuery {
|
||||
result := make([]repository.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = FeaturesSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req FeaturesSearchQuery) GetKey() repository.ColumnKey {
|
||||
return FeaturesSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req FeaturesSearchQuery) GetMethod() domain.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req FeaturesSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key FeaturesSearchKey) ToColumnName() string {
|
||||
switch model.FeaturesSearchKey(key) {
|
||||
case model.FeaturesSearchKeyAggregateID:
|
||||
return FeaturesKeyAggregateID
|
||||
case model.FeaturesSearchKeyDefault:
|
||||
return FeaturesKeyDefault
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
v1 "github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
||||
mgmt_view "github.com/caos/zitadel/internal/management/repository/eventsourcing/view"
|
||||
)
|
||||
|
||||
type FeaturesRepo struct {
|
||||
Eventstore v1.Eventstore
|
||||
|
||||
View *mgmt_view.View
|
||||
|
||||
SearchLimit uint64
|
||||
SystemDefaults systemdefaults.SystemDefaults
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) GetDefaultFeatures(ctx context.Context) (*features_model.FeaturesView, error) {
|
||||
features, viewErr := repo.View.FeaturesByAggregateID(domain.IAMID)
|
||||
if viewErr != nil && !errors.IsNotFound(viewErr) {
|
||||
return nil, viewErr
|
||||
}
|
||||
if errors.IsNotFound(viewErr) {
|
||||
features = new(model.FeaturesView)
|
||||
}
|
||||
events, esErr := repo.getIAMEvents(ctx, features.Sequence)
|
||||
if errors.IsNotFound(viewErr) && len(events) == 0 {
|
||||
return nil, errors.ThrowNotFound(nil, "EVENT-Lsoj7", "Errors.Org.NotFound")
|
||||
}
|
||||
if esErr != nil {
|
||||
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
|
||||
return model.FeaturesToModel(features), nil
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return model.FeaturesToModel(&featuresCopy), nil
|
||||
}
|
||||
}
|
||||
return model.FeaturesToModel(&featuresCopy), nil
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) GetOrgFeatures(ctx context.Context, orgID string) (*features_model.FeaturesView, error) {
|
||||
features, err := repo.View.FeaturesByAggregateID(orgID)
|
||||
if errors.IsNotFound(err) {
|
||||
return repo.GetDefaultFeatures(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.FeaturesToModel(features), err
|
||||
}
|
||||
|
||||
func (repo *FeaturesRepo) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) {
|
||||
query, err := iam_view.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.Eventstore.FilterEvents(ctx, query)
|
||||
}
|
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/caos/zitadel/internal/user/repository/view"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
@@ -93,8 +94,8 @@ func (repo *OrgRepository) SearchMyOrgDomains(ctx context.Context, request *org_
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*org_model.OrgChanges, error) {
|
||||
changes, err := repo.getOrgChanges(ctx, id, lastSequence, limit, sortAscending)
|
||||
func (repo *OrgRepository) OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error) {
|
||||
changes, err := repo.getOrgChanges(ctx, id, lastSequence, limit, sortAscending, auditLogRetention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -537,8 +538,8 @@ func (repo *OrgRepository) GetMailTexts(ctx context.Context) (*iam_model.MailTex
|
||||
return iam_es_model.MailTextsViewToModel(texts, defaultIn), err
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) getOrgChanges(ctx context.Context, orgID string, lastSequence uint64, limit uint64, sortAscending bool) (*org_model.OrgChanges, error) {
|
||||
query := org_view.ChangesQuery(orgID, lastSequence, limit, sortAscending)
|
||||
func (repo *OrgRepository) getOrgChanges(ctx context.Context, orgID string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error) {
|
||||
query := org_view.ChangesQuery(orgID, lastSequence, limit, sortAscending, auditLogRetention)
|
||||
|
||||
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
|
||||
if err != nil {
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
@@ -180,8 +181,8 @@ func (repo *ProjectRepo) SearchProjectRoles(ctx context.Context, projectID strin
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ProjectChanges, error) {
|
||||
changes, err := repo.getProjectChanges(ctx, id, lastSequence, limit, sortAscending)
|
||||
func (repo *ProjectRepo) ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ProjectChanges, error) {
|
||||
changes, err := repo.getProjectChanges(ctx, id, lastSequence, limit, sortAscending, retention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -254,8 +255,8 @@ func (repo *ProjectRepo) SearchApplications(ctx context.Context, request *proj_m
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) ApplicationChanges(ctx context.Context, id string, appId string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ApplicationChanges, error) {
|
||||
changes, err := repo.getApplicationChanges(ctx, id, appId, lastSequence, limit, sortAscending)
|
||||
func (repo *ProjectRepo) ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ApplicationChanges, error) {
|
||||
changes, err := repo.getApplicationChanges(ctx, projectID, appID, lastSequence, limit, sortAscending, retention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -505,8 +506,8 @@ func (r *ProjectRepo) getUserEvents(ctx context.Context, userID string, sequence
|
||||
return r.Eventstore.FilterEvents(ctx, query)
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) getProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ProjectChanges, error) {
|
||||
query := proj_view.ChangesQuery(id, lastSequence, limit, sortAscending)
|
||||
func (repo *ProjectRepo) getProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ProjectChanges, error) {
|
||||
query := proj_view.ChangesQuery(id, lastSequence, limit, sortAscending, retention)
|
||||
|
||||
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
|
||||
if err != nil {
|
||||
@@ -561,8 +562,8 @@ func (repo *ProjectRepo) getProjectEvents(ctx context.Context, id string, sequen
|
||||
return repo.Eventstore.FilterEvents(ctx, query)
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) getApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ApplicationChanges, error) {
|
||||
query := proj_view.ChangesQuery(projectID, lastSequence, limit, sortAscending)
|
||||
func (repo *ProjectRepo) getApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ApplicationChanges, error) {
|
||||
query := proj_view.ChangesQuery(projectID, lastSequence, limit, sortAscending, retention)
|
||||
|
||||
events, err := repo.Eventstore.FilterEvents(ctx, query)
|
||||
if err != nil {
|
||||
|
@@ -2,6 +2,8 @@ package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
@@ -82,8 +84,8 @@ func (repo *UserRepo) UserIDsByDomain(ctx context.Context, domain string) ([]str
|
||||
return repo.View.UserIDsByDomain(domain)
|
||||
}
|
||||
|
||||
func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*usr_model.UserChanges, error) {
|
||||
changes, err := repo.getUserChanges(ctx, id, lastSequence, limit, sortAscending)
|
||||
func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*usr_model.UserChanges, error) {
|
||||
changes, err := repo.getUserChanges(ctx, id, lastSequence, limit, sortAscending, retention)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -280,8 +282,8 @@ func (repo *UserRepo) SearchUserMemberships(ctx context.Context, request *usr_mo
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool) (*usr_model.UserChanges, error) {
|
||||
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending)
|
||||
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*usr_model.UserChanges, error) {
|
||||
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention)
|
||||
|
||||
events, err := r.Eventstore.FilterEvents(ctx, query)
|
||||
if err != nil {
|
||||
|
165
internal/management/repository/eventsourcing/handler/features.go
Normal file
165
internal/management/repository/eventsourcing/handler/features.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
iam_repo "github.com/caos/zitadel/internal/repository/iam"
|
||||
org_repo "github.com/caos/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "management.features"
|
||||
)
|
||||
|
||||
type Features struct {
|
||||
handler
|
||||
subscription *v1.Subscription
|
||||
}
|
||||
|
||||
func newFeatures(handler handler) *Features {
|
||||
h := &Features{
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (p *Features) subscribe() {
|
||||
p.subscription = p.es.Subscribe(p.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range p.subscription.Events {
|
||||
query.ReduceEvent(p, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Features) ViewModel() string {
|
||||
return featuresTable
|
||||
}
|
||||
|
||||
func (p *Features) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{iam_es_model.IAMAggregate, org_es_model.OrgAggregate}
|
||||
}
|
||||
|
||||
func (p *Features) EventQuery() (*es_models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(p.AggregateTypes()...).
|
||||
LatestSequenceFilter(sequence.CurrentSequence), nil
|
||||
}
|
||||
|
||||
func (p *Features) CurrentSequence() (uint64, error) {
|
||||
sequence, err := p.view.GetLatestFeaturesSequence()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (p *Features) Reduce(event *es_models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case org_es_model.OrgAggregate, iam_es_model.IAMAggregate:
|
||||
err = p.processFeatures(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Features) processFeatures(event *es_models.Event) (err error) {
|
||||
features := new(model.FeaturesView)
|
||||
switch string(event.Type) {
|
||||
case string(org_es_model.OrgAdded):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
case string(iam_repo.FeaturesSetEventType):
|
||||
defaultFeatures, err := p.view.AllDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, features := range defaultFeatures {
|
||||
err = features.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return p.view.PutFeaturesList(defaultFeatures, event)
|
||||
case string(org_repo.FeaturesSetEventType):
|
||||
features, err = p.view.FeaturesByAggregateID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = features.AppendEvent(event)
|
||||
case string(org_repo.FeaturesRemovedEventType):
|
||||
features, err = p.getDefaultFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
features.AggregateID = event.AggregateID
|
||||
features.Default = true
|
||||
default:
|
||||
return p.view.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutFeatures(features, event)
|
||||
}
|
||||
|
||||
func (p *Features) OnError(event *es_models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-Wj8sf", "id", event.AggregateID).WithError(err).Warn("something went wrong in login features handler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestFeaturesFailedEvent, p.view.ProcessedFeaturesFailedEvent, p.view.ProcessedFeaturesSequence, p.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (p *Features) OnSuccess() error {
|
||||
return spooler.HandleSuccess(p.view.UpdateFeaturesSpoolerRunTimestamp)
|
||||
}
|
||||
|
||||
func (p *Features) getDefaultFeatures() (*model.FeaturesView, error) {
|
||||
features, featuresErr := p.view.FeaturesByAggregateID(domain.IAMID)
|
||||
if featuresErr != nil && !caos_errs.IsNotFound(featuresErr) {
|
||||
return nil, featuresErr
|
||||
}
|
||||
if features == nil {
|
||||
features = &model.FeaturesView{}
|
||||
}
|
||||
events, err := p.getIAMEvents(features.Sequence)
|
||||
if err != nil {
|
||||
return features, featuresErr
|
||||
}
|
||||
featuresCopy := *features
|
||||
for _, event := range events {
|
||||
if err := featuresCopy.AppendEvent(event); err != nil {
|
||||
return features, nil
|
||||
}
|
||||
}
|
||||
return &featuresCopy, nil
|
||||
}
|
||||
|
||||
func (p *Features) getIAMEvents(sequence uint64) ([]*es_models.Event, error) {
|
||||
query, err := eventsourcing.IAMByIDQuery(domain.IAMID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.es.FilterEvents(context.Background(), query)
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||
@@ -76,6 +77,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
|
||||
handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}),
|
||||
newMailText(
|
||||
handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}),
|
||||
newFeatures(
|
||||
handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,7 @@ type EsRepository struct {
|
||||
eventstore.UserRepo
|
||||
eventstore.UserGrantRepo
|
||||
eventstore.IAMRepository
|
||||
eventstore.FeaturesRepo
|
||||
view *mgmt_view.View
|
||||
}
|
||||
|
||||
@@ -54,9 +55,8 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie
|
||||
ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID},
|
||||
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults},
|
||||
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, view},
|
||||
IAMRepository: eventstore.IAMRepository{
|
||||
IAMV2Query: queries,
|
||||
},
|
||||
IAMRepository: eventstore.IAMRepository{IAMV2Query: queries},
|
||||
FeaturesRepo: eventstore.FeaturesRepo{es, view, conf.SearchLimit, systemDefaults},
|
||||
view: view,
|
||||
}, nil
|
||||
}
|
||||
|
@@ -0,0 +1,56 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/features/repository/view"
|
||||
"github.com/caos/zitadel/internal/features/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresTable = "management.features"
|
||||
)
|
||||
|
||||
func (v *View) AllDefaultFeatures() ([]*model.FeaturesView, error) {
|
||||
return view.GetDefaultFeatures(v.Db, featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) FeaturesByAggregateID(aggregateID string) (*model.FeaturesView, error) {
|
||||
return view.GetFeaturesByAggregateID(v.Db, featuresTable, aggregateID)
|
||||
}
|
||||
|
||||
func (v *View) PutFeatures(features *model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeatures(v.Db, featuresTable, features)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) PutFeaturesList(features []*model.FeaturesView, event *models.Event) error {
|
||||
err := view.PutFeaturesList(v.Db, featuresTable, features...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedFeaturesSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesSequence() (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(featuresTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateFeaturesSpoolerRunTimestamp() error {
|
||||
return v.updateSpoolerRunSequence(featuresTable)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestFeaturesFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(featuresTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedFeaturesFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
11
internal/management/repository/features.go
Normal file
11
internal/management/repository/features.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
features_model "github.com/caos/zitadel/internal/features/model"
|
||||
)
|
||||
|
||||
type FeaturesRepository interface {
|
||||
GetOrgFeatures(ctx context.Context, id string) (*features_model.FeaturesView, error)
|
||||
}
|
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
type OrgRepository interface {
|
||||
OrgByID(ctx context.Context, id string) (*org_model.OrgView, error)
|
||||
OrgByDomainGlobal(ctx context.Context, domain string) (*org_model.OrgView, error)
|
||||
OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*org_model.OrgChanges, error)
|
||||
OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error)
|
||||
|
||||
SearchMyOrgDomains(ctx context.Context, request *org_model.OrgDomainSearchRequest) (*org_model.OrgDomainSearchResponse, error)
|
||||
|
||||
|
@@ -2,6 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
@@ -22,11 +24,11 @@ type ProjectRepository interface {
|
||||
GetProjectMemberRoles(ctx context.Context) ([]string, error)
|
||||
|
||||
SearchProjectRoles(ctx context.Context, projectId string, request *model.ProjectRoleSearchRequest) (*model.ProjectRoleSearchResponse, error)
|
||||
ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*model.ProjectChanges, error)
|
||||
ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.ProjectChanges, error)
|
||||
|
||||
ApplicationByID(ctx context.Context, projectID, appID string) (*model.ApplicationView, error)
|
||||
SearchApplications(ctx context.Context, request *model.ApplicationSearchRequest) (*model.ApplicationSearchResponse, error)
|
||||
ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool) (*model.ApplicationChanges, error)
|
||||
ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.ApplicationChanges, error)
|
||||
SearchClientKeys(ctx context.Context, request *key_model.AuthNKeySearchRequest) (*key_model.AuthNKeySearchResponse, error)
|
||||
GetClientKey(ctx context.Context, projectID, applicationID, keyID string) (*key_model.AuthNKeyView, error)
|
||||
|
||||
|
@@ -7,4 +7,5 @@ type Repository interface {
|
||||
UserRepository
|
||||
UserGrantRepository
|
||||
IamRepository
|
||||
FeaturesRepository
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
@@ -15,7 +16,7 @@ type UserRepository interface {
|
||||
GetUserByLoginNameGlobal(ctx context.Context, email string) (*model.UserView, error)
|
||||
IsUserUnique(ctx context.Context, userName, email string) (bool, error)
|
||||
|
||||
UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error)
|
||||
UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error)
|
||||
|
||||
ProfileByID(ctx context.Context, userID string) (*model.Profile, error)
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
@@ -36,13 +38,16 @@ func OrgNameUniqueQuery(name string) *es_models.SearchQuery {
|
||||
SetLimit(1)
|
||||
}
|
||||
|
||||
func ChangesQuery(orgID string, latestSequence, limit uint64, sortAscending bool) *es_models.SearchQuery {
|
||||
func ChangesQuery(orgID string, latestSequence, limit uint64, sortAscending bool, auditLogRetention time.Duration) *es_models.SearchQuery {
|
||||
query := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(model.OrgAggregate)
|
||||
|
||||
if !sortAscending {
|
||||
query.OrderDesc()
|
||||
}
|
||||
if auditLogRetention > 0 {
|
||||
query.CreationDateNewerFilter(time.Now().Add(-auditLogRetention))
|
||||
}
|
||||
|
||||
query.LatestSequenceFilter(latestSequence).
|
||||
AggregateIDFilter(orgID).
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
||||
@@ -20,12 +22,15 @@ func ProjectQuery(latestSequence uint64) *es_models.SearchQuery {
|
||||
LatestSequenceFilter(latestSequence)
|
||||
}
|
||||
|
||||
func ChangesQuery(projectID string, latestSequence, limit uint64, sortAscending bool) *es_models.SearchQuery {
|
||||
func ChangesQuery(projectID string, latestSequence, limit uint64, sortAscending bool, retention time.Duration) *es_models.SearchQuery {
|
||||
query := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(model.ProjectAggregate)
|
||||
if !sortAscending {
|
||||
query.OrderDesc()
|
||||
}
|
||||
if retention > 0 {
|
||||
query.CreationDateNewerFilter(time.Now().Add(-retention))
|
||||
}
|
||||
|
||||
query.LatestSequenceFilter(latestSequence).
|
||||
AggregateIDFilter(projectID).
|
||||
|
@@ -14,4 +14,5 @@ extend google.protobuf.MethodOptions {
|
||||
message AuthOption {
|
||||
string permission = 1;
|
||||
string check_field_name = 2;
|
||||
string feature = 3;
|
||||
}
|
@@ -21,10 +21,11 @@ const {{$s.Name}}_MethodPrefix = "{{$.File.Package}}.{{$s.Name}}"
|
||||
var {{$s.Name}}_AuthMethods = authz.MethodMapping {
|
||||
{{ range $m := $s.Method}}
|
||||
{{ $mAuthOpt := option $m.Options "zitadel.v1.auth_option" }}
|
||||
{{ if and $mAuthOpt $mAuthOpt.Permission }}
|
||||
{{ if and $mAuthOpt (or $mAuthOpt.Permission $mAuthOpt.Feature) }}
|
||||
"/{{$.File.Package}}.{{$s.Name}}/{{.Name}}": authz.Option{
|
||||
Permission: "{{$mAuthOpt.Permission}}",
|
||||
CheckParam: "{{$mAuthOpt.CheckFieldName}}",
|
||||
Feature: "{{$mAuthOpt.Feature}}",
|
||||
},
|
||||
{{end}}
|
||||
{{ end}}
|
||||
|
169
internal/repository/features/features.go
Normal file
169
internal/repository/features/features.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package features
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
featuresPrefix = "features."
|
||||
FeaturesSetEventType = featuresPrefix + "set"
|
||||
FeaturesRemovedEventType = featuresPrefix + "removed"
|
||||
)
|
||||
|
||||
type FeaturesSetEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
TierName *string `json:"tierName,omitempty"`
|
||||
TierDescription *string `json:"tierDescription,omitempty"`
|
||||
State *domain.FeaturesState `json:"state,omitempty"`
|
||||
StateDescription *string `json:"stateDescription,omitempty"`
|
||||
AuditLogRetention *time.Duration `json:"auditLogRetention,omitempty"`
|
||||
LoginPolicyFactors *bool `json:"loginPolicyFactors,omitempty"`
|
||||
LoginPolicyIDP *bool `json:"loginPolicyIDP,omitempty"`
|
||||
LoginPolicyPasswordless *bool `json:"loginPolicyPasswordless,omitempty"`
|
||||
LoginPolicyRegistration *bool `json:"loginPolicyRegistration,omitempty"`
|
||||
LoginPolicyUsernameLogin *bool `json:"loginPolicyUsername_login,omitempty"`
|
||||
PasswordComplexityPolicy *bool `json:"passwordComplexityPolicy,omitempty"`
|
||||
LabelPolicy *bool `json:"labelPolicy,omitempty"`
|
||||
}
|
||||
|
||||
func (e *FeaturesSetEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *FeaturesSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFeaturesSetEvent(
|
||||
base *eventstore.BaseEvent,
|
||||
changes []FeaturesChanges,
|
||||
) (*FeaturesSetEvent, error) {
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "FEATURES-d34F4", "Errors.NoChangesFound")
|
||||
}
|
||||
changeEvent := &FeaturesSetEvent{
|
||||
BaseEvent: *base,
|
||||
}
|
||||
for _, change := range changes {
|
||||
change(changeEvent)
|
||||
}
|
||||
return changeEvent, nil
|
||||
}
|
||||
|
||||
type FeaturesChanges func(*FeaturesSetEvent)
|
||||
|
||||
func ChangeTierName(tierName string) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.TierName = &tierName
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeTierDescription(tierDescription string) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.TierDescription = &tierDescription
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeState(State domain.FeaturesState) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.State = &State
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeStateDescription(statusDescription string) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.StateDescription = &statusDescription
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeAuditLogRetention(retention time.Duration) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.AuditLogRetention = &retention
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeLoginPolicyFactors(loginPolicyFactors bool) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.LoginPolicyFactors = &loginPolicyFactors
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeLoginPolicyIDP(loginPolicyIDP bool) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.LoginPolicyIDP = &loginPolicyIDP
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeLoginPolicyPasswordless(loginPolicyPasswordless bool) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.LoginPolicyPasswordless = &loginPolicyPasswordless
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeLoginPolicyRegistration(loginPolicyRegistration bool) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.LoginPolicyRegistration = &loginPolicyRegistration
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeLoginPolicyUsernameLogin(loginPolicyUsernameLogin bool) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.LoginPolicyUsernameLogin = &loginPolicyUsernameLogin
|
||||
}
|
||||
}
|
||||
|
||||
func ChangePasswordComplexityPolicy(passwordComplexityPolicy bool) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.PasswordComplexityPolicy = &passwordComplexityPolicy
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeLabelPolicy(labelPolicy bool) func(event *FeaturesSetEvent) {
|
||||
return func(e *FeaturesSetEvent) {
|
||||
e.LabelPolicy = &labelPolicy
|
||||
}
|
||||
}
|
||||
|
||||
func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||
e := &FeaturesSetEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(event.Data, e)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "FEATURES-fdgDg", "unable to unmarshal features")
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
type FeaturesRemovedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *FeaturesRemovedEvent) Data() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *FeaturesRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFeaturesRemovedEvent(base *eventstore.BaseEvent) *FeaturesRemovedEvent {
|
||||
return &FeaturesRemovedEvent{
|
||||
BaseEvent: *base,
|
||||
}
|
||||
}
|
||||
|
||||
func FeaturesRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||
return &FeaturesRemovedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}, nil
|
||||
}
|
@@ -42,5 +42,6 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
RegisterFilterEventMapper(MailTemplateAddedEventType, MailTemplateAddedEventMapper).
|
||||
RegisterFilterEventMapper(MailTemplateChangedEventType, MailTemplateChangedEventMapper).
|
||||
RegisterFilterEventMapper(MailTextAddedEventType, MailTextAddedEventMapper).
|
||||
RegisterFilterEventMapper(MailTextChangedEventType, MailTextChangedEventMapper)
|
||||
RegisterFilterEventMapper(MailTextChangedEventType, MailTextChangedEventMapper).
|
||||
RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper)
|
||||
}
|
||||
|
43
internal/repository/iam/features.go
Normal file
43
internal/repository/iam/features.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||
"github.com/caos/zitadel/internal/repository/features"
|
||||
)
|
||||
|
||||
var (
|
||||
FeaturesSetEventType = iamEventTypePrefix + features.FeaturesSetEventType
|
||||
)
|
||||
|
||||
type FeaturesSetEvent struct {
|
||||
features.FeaturesSetEvent
|
||||
}
|
||||
|
||||
func NewFeaturesSetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
changes []features.FeaturesChanges,
|
||||
) (*FeaturesSetEvent, error) {
|
||||
changedEvent, err := features.NewFeaturesSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
FeaturesSetEventType),
|
||||
changes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FeaturesSetEvent{FeaturesSetEvent: *changedEvent}, nil
|
||||
}
|
||||
|
||||
func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||
e, err := features.FeaturesSetEventMapper(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FeaturesSetEvent{FeaturesSetEvent: *e.(*features.FeaturesSetEvent)}, nil
|
||||
}
|
@@ -55,5 +55,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
RegisterFilterEventMapper(IDPConfigDeactivatedEventType, IDPConfigDeactivatedEventMapper).
|
||||
RegisterFilterEventMapper(IDPConfigReactivatedEventType, IDPConfigReactivatedEventMapper).
|
||||
RegisterFilterEventMapper(IDPOIDCConfigAddedEventType, IDPOIDCConfigAddedEventMapper).
|
||||
RegisterFilterEventMapper(IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper)
|
||||
RegisterFilterEventMapper(IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper).
|
||||
RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper).
|
||||
RegisterFilterEventMapper(FeaturesRemovedEventType, FeaturesRemovedEventMapper)
|
||||
}
|
||||
|
71
internal/repository/org/features.go
Normal file
71
internal/repository/org/features.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||
"github.com/caos/zitadel/internal/repository/features"
|
||||
)
|
||||
|
||||
var (
|
||||
FeaturesSetEventType = orgEventTypePrefix + features.FeaturesSetEventType
|
||||
FeaturesRemovedEventType = orgEventTypePrefix + features.FeaturesRemovedEventType
|
||||
)
|
||||
|
||||
type FeaturesSetEvent struct {
|
||||
features.FeaturesSetEvent
|
||||
}
|
||||
|
||||
func NewFeaturesSetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
changes []features.FeaturesChanges,
|
||||
) (*FeaturesSetEvent, error) {
|
||||
changedEvent, err := features.NewFeaturesSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
FeaturesSetEventType),
|
||||
changes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FeaturesSetEvent{FeaturesSetEvent: *changedEvent}, nil
|
||||
}
|
||||
|
||||
func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||
e, err := features.FeaturesSetEventMapper(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FeaturesSetEvent{FeaturesSetEvent: *e.(*features.FeaturesSetEvent)}, nil
|
||||
}
|
||||
|
||||
type FeaturesRemovedEvent struct {
|
||||
features.FeaturesRemovedEvent
|
||||
}
|
||||
|
||||
func NewFeaturesRemovedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *FeaturesRemovedEvent {
|
||||
return &FeaturesRemovedEvent{
|
||||
FeaturesRemovedEvent: *features.NewFeaturesRemovedEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
FeaturesRemovedEventType),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func FeaturesRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||
e, err := features.FeaturesRemovedEventMapper(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FeaturesRemovedEvent{FeaturesRemovedEvent: *e.(*features.FeaturesRemovedEvent)}, nil
|
||||
}
|
@@ -17,6 +17,7 @@ type IAMSetUp struct {
|
||||
Step9 *command.Step9
|
||||
Step10 *command.Step10
|
||||
Step11 *command.Step11
|
||||
Step12 *command.Step12
|
||||
}
|
||||
|
||||
func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) {
|
||||
@@ -34,6 +35,7 @@ func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) {
|
||||
setup.Step9,
|
||||
setup.Step10,
|
||||
setup.Step11,
|
||||
setup.Step12,
|
||||
} {
|
||||
if step.Step() <= currentDone {
|
||||
continue
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
@@ -20,12 +22,15 @@ func UserQuery(latestSequence uint64) *es_models.SearchQuery {
|
||||
LatestSequenceFilter(latestSequence)
|
||||
}
|
||||
|
||||
func ChangesQuery(userID string, latestSequence, limit uint64, sortAscending bool) *es_models.SearchQuery {
|
||||
func ChangesQuery(userID string, latestSequence, limit uint64, sortAscending bool, retention time.Duration) *es_models.SearchQuery {
|
||||
query := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(model.UserAggregate)
|
||||
if !sortAscending {
|
||||
query.OrderDesc()
|
||||
}
|
||||
if retention > 0 {
|
||||
query.CreationDateNewerFilter(time.Now().Add(-retention))
|
||||
}
|
||||
|
||||
query.LatestSequenceFilter(latestSequence).
|
||||
AggregateIDFilter(userID).
|
||||
|
103
migrations/cockroach/V1.35__features.sql
Normal file
103
migrations/cockroach/V1.35__features.sql
Normal file
@@ -0,0 +1,103 @@
|
||||
CREATE TABLE adminapi.features
|
||||
(
|
||||
aggregate_id TEXT,
|
||||
|
||||
creation_date TIMESTAMPTZ,
|
||||
change_date TIMESTAMPTZ,
|
||||
sequence BIGINT,
|
||||
default_features BOOLEAN,
|
||||
|
||||
tier_name TEXT,
|
||||
tier_description TEXT,
|
||||
state SMALLINT,
|
||||
state_description TEXT,
|
||||
|
||||
audit_log_retention BIGINT,
|
||||
login_policy_factors BOOLEAN,
|
||||
login_policy_idp BOOLEAN,
|
||||
login_policy_passwordless BOOLEAN,
|
||||
login_policy_registration BOOLEAN,
|
||||
login_policy_username_login BOOLEAN,
|
||||
password_complexity_policy BOOLEAN,
|
||||
label_policy BOOLEAN,
|
||||
|
||||
PRIMARY KEY (aggregate_id)
|
||||
);
|
||||
|
||||
CREATE TABLE auth.features
|
||||
(
|
||||
aggregate_id TEXT,
|
||||
|
||||
creation_date TIMESTAMPTZ,
|
||||
change_date TIMESTAMPTZ,
|
||||
sequence BIGINT,
|
||||
default_features BOOLEAN,
|
||||
|
||||
tier_name TEXT,
|
||||
tier_description TEXT,
|
||||
state SMALLINT,
|
||||
state_description TEXT,
|
||||
|
||||
audit_log_retention BIGINT,
|
||||
login_policy_factors BOOLEAN,
|
||||
login_policy_idp BOOLEAN,
|
||||
login_policy_passwordless BOOLEAN,
|
||||
login_policy_registration BOOLEAN,
|
||||
login_policy_username_login BOOLEAN,
|
||||
password_complexity_policy BOOLEAN,
|
||||
label_policy BOOLEAN,
|
||||
|
||||
PRIMARY KEY (aggregate_id)
|
||||
);
|
||||
|
||||
CREATE TABLE authz.features
|
||||
(
|
||||
aggregate_id TEXT,
|
||||
|
||||
creation_date TIMESTAMPTZ,
|
||||
change_date TIMESTAMPTZ,
|
||||
sequence BIGINT,
|
||||
default_features BOOLEAN,
|
||||
|
||||
tier_name TEXT,
|
||||
tier_description TEXT,
|
||||
state SMALLINT,
|
||||
state_description TEXT,
|
||||
|
||||
audit_log_retention BIGINT,
|
||||
login_policy_factors BOOLEAN,
|
||||
login_policy_idp BOOLEAN,
|
||||
login_policy_passwordless BOOLEAN,
|
||||
login_policy_registration BOOLEAN,
|
||||
login_policy_username_login BOOLEAN,
|
||||
password_complexity_policy BOOLEAN,
|
||||
label_policy BOOLEAN,
|
||||
|
||||
PRIMARY KEY (aggregate_id)
|
||||
);
|
||||
|
||||
CREATE TABLE management.features
|
||||
(
|
||||
aggregate_id TEXT,
|
||||
|
||||
creation_date TIMESTAMPTZ,
|
||||
change_date TIMESTAMPTZ,
|
||||
sequence BIGINT,
|
||||
default_features BOOLEAN,
|
||||
|
||||
tier_name TEXT,
|
||||
tier_description TEXT,
|
||||
state SMALLINT,
|
||||
state_description TEXT,
|
||||
|
||||
audit_log_retention BIGINT,
|
||||
login_policy_factors BOOLEAN,
|
||||
login_policy_idp BOOLEAN,
|
||||
login_policy_passwordless BOOLEAN,
|
||||
login_policy_registration BOOLEAN,
|
||||
login_policy_username_login BOOLEAN,
|
||||
password_complexity_policy BOOLEAN,
|
||||
label_policy BOOLEAN,
|
||||
|
||||
PRIMARY KEY (aggregate_id)
|
||||
);
|
@@ -7,9 +7,11 @@ import "zitadel/options.proto";
|
||||
import "zitadel/org.proto";
|
||||
import "zitadel/policy.proto";
|
||||
import "zitadel/member.proto";
|
||||
import "zitadel/features.proto";
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/duration.proto";
|
||||
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
|
||||
@@ -171,6 +173,58 @@ service AdminService {
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetDefaultFeatures(GetDefaultFeaturesRequest) returns (GetDefaultFeaturesResponse) {
|
||||
option(google.api.http) = {
|
||||
get: "/features"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "iam.features.read"
|
||||
};
|
||||
}
|
||||
|
||||
rpc SetDefaultFeatures(SetDefaultFeaturesRequest) returns (SetDefaultFeaturesResponse) {
|
||||
option(google.api.http) = {
|
||||
put: "/features"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "iam.features.write"
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetOrgFeatures(GetOrgFeaturesRequest) returns (GetOrgFeaturesResponse) {
|
||||
option(google.api.http) = {
|
||||
get: "/orgs/{org_id}/features"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "iam.features.read"
|
||||
};
|
||||
}
|
||||
|
||||
rpc SetOrgFeatures(SetOrgFeaturesRequest) returns (SetOrgFeaturesResponse) {
|
||||
option(google.api.http) = {
|
||||
put: "/orgs/{org_id}/features"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "iam.features.write"
|
||||
};
|
||||
}
|
||||
|
||||
rpc ResetOrgFeatures(ResetOrgFeaturesRequest) returns (ResetOrgFeaturesResponse) {
|
||||
option(google.api.http) = {
|
||||
delete: "/orgs/{org_id}/features"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "iam.features.write"
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetOrgIAMPolicy(GetOrgIAMPolicyRequest) returns (GetOrgIAMPolicyResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/policies/orgiam"
|
||||
@@ -697,6 +751,66 @@ message UpdateIDPOIDCConfigResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
message GetDefaultFeaturesRequest {}
|
||||
|
||||
message GetDefaultFeaturesResponse {
|
||||
zitadel.features.v1.Features features = 1;
|
||||
}
|
||||
|
||||
message SetDefaultFeaturesRequest {
|
||||
string tier_name = 1 [(validate.rules).string = {max_len: 200}];
|
||||
string description = 2 [(validate.rules).string = {max_len: 200}];
|
||||
|
||||
google.protobuf.Duration audit_log_retention = 5 [(validate.rules).duration = {gte: {seconds: 0}}];
|
||||
bool login_policy_username_login = 6;
|
||||
bool login_policy_registration = 7;
|
||||
bool login_policy_idp = 8;
|
||||
bool login_policy_factors = 9;
|
||||
bool login_policy_passwordless = 10;
|
||||
bool password_complexity_policy = 11;
|
||||
bool label_policy = 12;
|
||||
}
|
||||
|
||||
message SetDefaultFeaturesResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
message GetOrgFeaturesRequest {
|
||||
string org_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
}
|
||||
|
||||
message GetOrgFeaturesResponse {
|
||||
zitadel.features.v1.Features features = 1;
|
||||
}
|
||||
|
||||
message SetOrgFeaturesRequest {
|
||||
string org_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
string tier_name = 2 [(validate.rules).string = {max_len: 200}];
|
||||
string description = 3 [(validate.rules).string = {max_len: 200}];
|
||||
zitadel.features.v1.FeaturesState state = 4;
|
||||
string state_description = 5 [(validate.rules).string = {max_len: 200}];
|
||||
|
||||
google.protobuf.Duration audit_log_retention = 6 [(validate.rules).duration = {gte: {seconds: 0}}];
|
||||
bool login_policy_username_login = 7;
|
||||
bool login_policy_registration = 8;
|
||||
bool login_policy_idp = 9;
|
||||
bool login_policy_factors = 10;
|
||||
bool login_policy_passwordless = 11;
|
||||
bool password_complexity_policy = 12;
|
||||
bool label_policy = 13;
|
||||
}
|
||||
|
||||
message SetOrgFeaturesResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
message ResetOrgFeaturesRequest {
|
||||
string org_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
}
|
||||
message ResetOrgFeaturesResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
message GetOrgIAMPolicyRequest {}
|
||||
|
||||
message GetOrgIAMPolicyResponse {
|
||||
|
@@ -375,6 +375,16 @@ service AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
rpc ListMyZitadelFeatures(ListMyZitadelFeaturesRequest) returns (ListMyZitadelFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/features/zitadel/me/_search"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated"
|
||||
};
|
||||
}
|
||||
|
||||
rpc ListMyZitadelPermissions(ListMyZitadelPermissionsRequest) returns (ListMyZitadelPermissionsResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/permissions/zitadel/me/_search"
|
||||
@@ -658,6 +668,12 @@ message ListMyProjectOrgsResponse {
|
||||
repeated zitadel.org.v1.Org result = 2;
|
||||
}
|
||||
|
||||
message ListMyZitadelFeaturesRequest {}
|
||||
|
||||
message ListMyZitadelFeaturesResponse {
|
||||
repeated string result = 1;
|
||||
}
|
||||
|
||||
message ListMyZitadelPermissionsRequest {}
|
||||
|
||||
message ListMyZitadelPermissionsResponse {
|
||||
|
39
proto/zitadel/features.proto
Normal file
39
proto/zitadel/features.proto
Normal file
@@ -0,0 +1,39 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "zitadel/object.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/duration.proto";
|
||||
|
||||
package zitadel.features.v1;
|
||||
|
||||
option go_package = "github.com/caos/zitadel/pkg/grpc/features";
|
||||
|
||||
message Features {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
FeatureTier tier = 2;
|
||||
bool is_default = 3;
|
||||
|
||||
google.protobuf.Duration audit_log_retention = 4;
|
||||
bool login_policy_username_login = 5;
|
||||
bool login_policy_registration = 6;
|
||||
bool login_policy_idp = 7;
|
||||
bool login_policy_factors = 8;
|
||||
bool login_policy_passwordless = 9;
|
||||
bool password_complexity_policy = 10;
|
||||
bool label_policy = 11;
|
||||
}
|
||||
|
||||
message FeatureTier {
|
||||
string name = 1;
|
||||
string description = 2;
|
||||
FeaturesState state = 3;
|
||||
string status_info = 4;
|
||||
}
|
||||
|
||||
|
||||
enum FeaturesState {
|
||||
FEATURES_STATE_ACTIVE = 0;
|
||||
FEATURES_STATE_ACTION_REQUIRED = 1;
|
||||
FEATURES_STATE_CANCELED = 2;
|
||||
FEATURES_STATE_GRANDFATHERED = 3;
|
||||
}
|
@@ -12,6 +12,7 @@ import "zitadel/policy.proto";
|
||||
import "zitadel/message.proto";
|
||||
import "zitadel/change.proto";
|
||||
import "zitadel/auth_n_key.proto";
|
||||
import "zitadel/features.proto";
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
@@ -1340,6 +1341,16 @@ service ManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/features"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "features.read"
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetOrgIAMPolicy(GetOrgIAMPolicyRequest) returns (GetOrgIAMPolicyResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/policies/orgiam"
|
||||
@@ -1378,6 +1389,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1389,6 +1401,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1421,6 +1434,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1431,6 +1445,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1452,6 +1467,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy.factors"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1462,6 +1478,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy.factors"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1483,6 +1500,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy.factors"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1493,6 +1511,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "login_policy.factors"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1524,6 +1543,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "password_complexity_policy"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1535,6 +1555,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "password_complexity_policy"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1680,6 +1701,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "label_policy"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1691,6 +1713,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "policy.write"
|
||||
feature: "label_policy"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1733,6 +1756,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "org.idp.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1744,6 +1768,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "org.idp.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1755,6 +1780,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "org.idp.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1765,6 +1791,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "org.idp.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1776,6 +1803,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "org.idp.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1787,6 +1815,7 @@ service ManagementService {
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "org.idp.write"
|
||||
feature: "login_policy.idp"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2956,6 +2985,12 @@ message BulkRemoveUserGrantRequest {
|
||||
|
||||
message BulkRemoveUserGrantResponse {}
|
||||
|
||||
message GetFeaturesRequest {}
|
||||
|
||||
message GetFeaturesResponse {
|
||||
zitadel.features.v1.Features features = 1;
|
||||
}
|
||||
|
||||
message GetOrgIAMPolicyRequest {}
|
||||
|
||||
message GetOrgIAMPolicyResponse {
|
||||
|
@@ -14,4 +14,5 @@ extend google.protobuf.MethodOptions {
|
||||
message AuthOption {
|
||||
string permission = 1;
|
||||
string check_field_name = 2;
|
||||
string feature = 3;
|
||||
}
|
Reference in New Issue
Block a user