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:
|
Permissions:
|
||||||
- "iam.read"
|
- "iam.read"
|
||||||
- "iam.write"
|
- "iam.write"
|
||||||
|
- "iam.features.read"
|
||||||
|
- "iam.features.write"
|
||||||
- "iam.policy.read"
|
- "iam.policy.read"
|
||||||
- "iam.policy.write"
|
- "iam.policy.write"
|
||||||
- "iam.policy.delete"
|
- "iam.policy.delete"
|
||||||
@@ -31,6 +33,7 @@ InternalAuthZ:
|
|||||||
- "user.grant.write"
|
- "user.grant.write"
|
||||||
- "user.grant.delete"
|
- "user.grant.delete"
|
||||||
- "user.membership.read"
|
- "user.membership.read"
|
||||||
|
- "features.read"
|
||||||
- "policy.read"
|
- "policy.read"
|
||||||
- "policy.write"
|
- "policy.write"
|
||||||
- "policy.delete"
|
- "policy.delete"
|
||||||
@@ -56,6 +59,7 @@ InternalAuthZ:
|
|||||||
- Role: 'IAM_OWNER_VIEWER'
|
- Role: 'IAM_OWNER_VIEWER'
|
||||||
Permissions:
|
Permissions:
|
||||||
- "iam.read"
|
- "iam.read"
|
||||||
|
- "iam.features.read"
|
||||||
- "iam.policy.read"
|
- "iam.policy.read"
|
||||||
- "iam.member.read"
|
- "iam.member.read"
|
||||||
- "iam.idp.read"
|
- "iam.idp.read"
|
||||||
@@ -66,6 +70,7 @@ InternalAuthZ:
|
|||||||
- "user.global.read"
|
- "user.global.read"
|
||||||
- "user.grant.read"
|
- "user.grant.read"
|
||||||
- "user.membership.read"
|
- "user.membership.read"
|
||||||
|
- "features.read"
|
||||||
- "policy.read"
|
- "policy.read"
|
||||||
- "project.read"
|
- "project.read"
|
||||||
- "project.member.read"
|
- "project.member.read"
|
||||||
@@ -93,6 +98,7 @@ InternalAuthZ:
|
|||||||
- "user.grant.write"
|
- "user.grant.write"
|
||||||
- "user.grant.delete"
|
- "user.grant.delete"
|
||||||
- "user.membership.read"
|
- "user.membership.read"
|
||||||
|
- "features.read"
|
||||||
- "policy.read"
|
- "policy.read"
|
||||||
- "policy.write"
|
- "policy.write"
|
||||||
- "policy.delete"
|
- "policy.delete"
|
||||||
@@ -123,6 +129,7 @@ InternalAuthZ:
|
|||||||
- "user.global.read"
|
- "user.global.read"
|
||||||
- "user.grant.read"
|
- "user.grant.read"
|
||||||
- "user.membership.read"
|
- "user.membership.read"
|
||||||
|
- "features.read"
|
||||||
- "policy.read"
|
- "policy.read"
|
||||||
- "project.read"
|
- "project.read"
|
||||||
- "project.member.read"
|
- "project.member.read"
|
||||||
|
@@ -103,24 +103,24 @@ func startZitadel(configPaths []string) {
|
|||||||
logging.Log("MAIN-FaF2r").OnError(err).Fatal("cannot read config")
|
logging.Log("MAIN-FaF2r").OnError(err).Fatal("cannot read config")
|
||||||
|
|
||||||
ctx := context.Background()
|
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)
|
esQueries, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Queries.Eventstore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start eventstore for queries")
|
||||||
}
|
}
|
||||||
queries, err := query.StartQueries(esQueries, conf.SystemDefaults)
|
queries, err := query.StartQueries(esQueries, conf.SystemDefaults)
|
||||||
if err != nil {
|
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)
|
authZRepo, err := authz.Start(ctx, conf.AuthZ, conf.InternalAuthZ, conf.SystemDefaults, queries)
|
||||||
logging.Log("MAIN-s9KOw").OnError(err).Fatal("error starting authz repo")
|
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
|
var authRepo *auth_es.EsRepository
|
||||||
if *authEnabled || *oidcEnabled || *loginEnabled {
|
if *authEnabled || *oidcEnabled || *loginEnabled {
|
||||||
authRepo, err = auth_es.Start(conf.Auth, conf.InternalAuthZ, conf.SystemDefaults, commands, queries, authZRepo, esQueries)
|
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)
|
es, err := eventstore.Start(conf.Eventstore)
|
||||||
logging.Log("MAIN-Ddt3").OnError(err).Fatal("cannot start 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")
|
logging.Log("MAIN-dsjrr").OnError(err).Fatal("cannot start command side")
|
||||||
|
|
||||||
err = setup.Execute(ctx, conf.SetUp, conf.SystemDefaults.IamID, commands)
|
err = setup.Execute(ctx, conf.SetUp, conf.SystemDefaults.IamID, commands)
|
||||||
|
@@ -175,3 +175,6 @@ SetUp:
|
|||||||
ButtonText: Login
|
ButtonText: Login
|
||||||
Step11:
|
Step11:
|
||||||
MigrateV1EventstoreToV2: $ZITADEL_MIGRATE_ES_V1
|
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
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/admin/repository/eventsourcing/view"
|
"github.com/caos/zitadel/internal/admin/repository/eventsourcing/view"
|
||||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||||
"github.com/caos/zitadel/internal/config/types"
|
"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}),
|
handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}),
|
||||||
newMailText(
|
newMailText(
|
||||||
handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}),
|
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.OrgRepo
|
||||||
eventstore.IAMRepository
|
eventstore.IAMRepository
|
||||||
eventstore.AdministratorRepo
|
eventstore.AdministratorRepo
|
||||||
|
eventstore.FeaturesRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, roles []string) (*EsRepository, error) {
|
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{
|
AdministratorRepo: eventstore.AdministratorRepo{
|
||||||
View: view,
|
View: view,
|
||||||
},
|
},
|
||||||
|
FeaturesRepo: eventstore.FeaturesRepo{
|
||||||
|
Eventstore: es,
|
||||||
|
View: view,
|
||||||
|
SearchLimit: conf.SearchLimit,
|
||||||
|
SystemDefaults: systemDefaults,
|
||||||
|
},
|
||||||
}, nil
|
}, 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
|
OrgRepository
|
||||||
IAMRepository
|
IAMRepository
|
||||||
AdministratorRepository
|
AdministratorRepository
|
||||||
|
FeaturesRepository
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,13 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if requiredAuthOption.Feature != "" {
|
||||||
|
err = CheckOrgFeatures(ctx, verifier, ctxData.OrgID, requiredAuthOption.Feature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if requiredAuthOption.Permission == authenticated {
|
if requiredAuthOption.Permission == authenticated {
|
||||||
return func(parent context.Context) context.Context {
|
return func(parent context.Context) context.Context {
|
||||||
return context.WithValue(parent, dataKey, ctxData)
|
return context.WithValue(parent, dataKey, ctxData)
|
||||||
@@ -49,6 +56,10 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
|
|||||||
}, nil
|
}, 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 {
|
func checkUserPermissions(req interface{}, userPerms []string, authOpt Option) error {
|
||||||
if len(userPerms) == 0 {
|
if len(userPerms) == 0 {
|
||||||
return errors.ThrowPermissionDenied(nil, "AUTH-5mWD2", "No matching permissions found")
|
return errors.ThrowPermissionDenied(nil, "AUTH-5mWD2", "No matching permissions found")
|
||||||
|
@@ -14,6 +14,7 @@ type MethodMapping map[string]Option
|
|||||||
type Option struct {
|
type Option struct {
|
||||||
Permission string
|
Permission string
|
||||||
CheckParam string
|
CheckParam string
|
||||||
|
Feature string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Config) getPermissionsFromRole(role string) []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
|
return "clientID", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *testVerifier) CheckOrgFeatures(context.Context, string, ...string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func equalStringArray(a, b []string) bool {
|
func equalStringArray(a, b []string) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
|
@@ -25,6 +25,7 @@ type authZRepo interface {
|
|||||||
SearchMyMemberships(ctx context.Context) ([]*Membership, error)
|
SearchMyMemberships(ctx context.Context) ([]*Membership, error)
|
||||||
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
||||||
ExistsOrg(ctx context.Context, orgID string) error
|
ExistsOrg(ctx context.Context, orgID string) error
|
||||||
|
CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(authZRepo authZRepo) (v *TokenVerifier) {
|
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
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/admin/repository"
|
"github.com/caos/zitadel/internal/admin/repository"
|
||||||
"github.com/caos/zitadel/internal/admin/repository/eventsourcing"
|
"github.com/caos/zitadel/internal/admin/repository/eventsourcing"
|
||||||
"github.com/caos/zitadel/internal/api/authz"
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
@@ -8,7 +10,6 @@ import (
|
|||||||
"github.com/caos/zitadel/internal/command"
|
"github.com/caos/zitadel/internal/command"
|
||||||
"github.com/caos/zitadel/internal/query"
|
"github.com/caos/zitadel/internal/query"
|
||||||
"github.com/caos/zitadel/pkg/grpc/admin"
|
"github.com/caos/zitadel/pkg/grpc/admin"
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,6 +26,7 @@ type Server struct {
|
|||||||
iam repository.IAMRepository
|
iam repository.IAMRepository
|
||||||
administrator repository.AdministratorRepository
|
administrator repository.AdministratorRepository
|
||||||
repo repository.Repository
|
repo repository.Repository
|
||||||
|
features repository.FeaturesRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -39,6 +41,7 @@ func CreateServer(command *command.Commands, query *query.Queries, repo reposito
|
|||||||
iam: repo,
|
iam: repo,
|
||||||
administrator: repo,
|
administrator: repo,
|
||||||
repo: 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) {
|
func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserChangesRequest) (*auth_pb.ListMyUserChangesResponse, error) {
|
||||||
offset, limit, asc := object.ListQueryToModel(req.Query)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChangesRequest) (*mgmt_pb.ListOrgChangesResponse, error) {
|
||||||
offset, limit, asc := object.ListQueryToModel(req.Query)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjectChangesRequest) (*mgmt_pb.ListProjectChangesResponse, error) {
|
||||||
offset, limit, asc := object_grpc.ListQueryToModel(req.Query)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChangesRequest) (*mgmt_pb.ListAppChangesResponse, error) {
|
||||||
offset, limit, asc := object_grpc.ListQueryToModel(req.Query)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package management
|
package management
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/api/authz"
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
"github.com/caos/zitadel/internal/api/grpc/server"
|
"github.com/caos/zitadel/internal/api/grpc/server"
|
||||||
"github.com/caos/zitadel/internal/command"
|
"github.com/caos/zitadel/internal/command"
|
||||||
@@ -9,7 +11,6 @@ import (
|
|||||||
"github.com/caos/zitadel/internal/management/repository/eventsourcing"
|
"github.com/caos/zitadel/internal/management/repository/eventsourcing"
|
||||||
"github.com/caos/zitadel/internal/query"
|
"github.com/caos/zitadel/internal/query"
|
||||||
"github.com/caos/zitadel/pkg/grpc/management"
|
"github.com/caos/zitadel/pkg/grpc/management"
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,6 +28,7 @@ type Server struct {
|
|||||||
user repository.UserRepository
|
user repository.UserRepository
|
||||||
usergrant repository.UserGrantRepository
|
usergrant repository.UserGrantRepository
|
||||||
iam repository.IamRepository
|
iam repository.IamRepository
|
||||||
|
features repository.FeaturesRepository
|
||||||
authZ authz.Config
|
authZ authz.Config
|
||||||
systemDefaults systemdefaults.SystemDefaults
|
systemDefaults systemdefaults.SystemDefaults
|
||||||
}
|
}
|
||||||
@@ -44,6 +46,7 @@ func CreateServer(command *command.Commands, query *query.Queries, repo reposito
|
|||||||
user: repo,
|
user: repo,
|
||||||
usergrant: repo,
|
usergrant: repo,
|
||||||
iam: repo,
|
iam: repo,
|
||||||
|
features: repo,
|
||||||
systemDefaults: sd,
|
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) {
|
func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChangesRequest) (*mgmt_pb.ListUserChangesResponse, error) {
|
||||||
offset, limit, asc := object.ListQueryToModel(req.Query)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ func ModelLoginPolicyToPb(policy *model.LoginPolicyView) *policy_pb.LoginPolicy
|
|||||||
IsDefault: policy.Default,
|
IsDefault: policy.Default,
|
||||||
AllowUsernamePassword: policy.AllowUsernamePassword,
|
AllowUsernamePassword: policy.AllowUsernamePassword,
|
||||||
AllowRegister: policy.AllowRegister,
|
AllowRegister: policy.AllowRegister,
|
||||||
AllowExternalIdp: policy.AllowRegister,
|
AllowExternalIdp: policy.AllowExternalIDP,
|
||||||
ForceMfa: policy.ForceMFA,
|
ForceMfa: policy.ForceMFA,
|
||||||
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
|
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) {
|
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
func (v *verifierMock) CheckOrgFeatures(context.Context, string, ...string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Test_authorize(t *testing.T) {
|
func Test_authorize(t *testing.T) {
|
||||||
type args struct {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||||
"github.com/golang/protobuf/ptypes"
|
"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
|
return usr_view_model.UserToModel(&userCopy), nil
|
||||||
}
|
}
|
||||||
func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error) {
|
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)
|
changes, err := repo.getUserChanges(ctx, authz.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending, retention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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) {
|
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)
|
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention)
|
||||||
|
|
||||||
events, err := r.Eventstore.FilterEvents(ctx, query)
|
events, err := r.Eventstore.FilterEvents(ctx, query)
|
||||||
if err != nil {
|
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}),
|
handler{view, bulkLimit, configs.cycleDuration("OrgIAMPolicy"), errorCount, es}),
|
||||||
newProjectRole(handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount, es}),
|
newProjectRole(handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount, es}),
|
||||||
newLabelPolicy(handler{view, bulkLimit, configs.cycleDuration("LabelPolicy"), 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.UserGrantRepo
|
||||||
eventstore.OrgRepository
|
eventstore.OrgRepository
|
||||||
eventstore.IAMRepository
|
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) {
|
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,
|
IAMID: systemDefaults.IamID,
|
||||||
IAMV2QuerySide: queries,
|
IAMV2QuerySide: queries,
|
||||||
},
|
},
|
||||||
|
eventstore.FeaturesRepo{
|
||||||
|
Eventstore: es,
|
||||||
|
View: view,
|
||||||
|
},
|
||||||
}, nil
|
}, 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
|
UserGrantRepository
|
||||||
OrgRepository
|
OrgRepository
|
||||||
IAMRepository
|
IAMRepository
|
||||||
|
FeaturesRepository
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
key_model "github.com/caos/zitadel/internal/key/model"
|
key_model "github.com/caos/zitadel/internal/key/model"
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ type myUserRepo interface {
|
|||||||
|
|
||||||
GetMyPasswordless(ctx context.Context) ([]*model.WebAuthNView, error)
|
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)
|
SearchMyUserMemberships(ctx context.Context, request *model.UserMembershipSearchRequest) (*model.UserMembershipSearchResponse, error)
|
||||||
}
|
}
|
||||||
|
@@ -2,26 +2,26 @@ package eventstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
|
||||||
usr_view "github.com/caos/zitadel/internal/user/repository/view"
|
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
|
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
|
||||||
"github.com/caos/zitadel/internal/crypto"
|
"github.com/caos/zitadel/internal/crypto"
|
||||||
|
"github.com/caos/zitadel/internal/domain"
|
||||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
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"
|
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
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"
|
"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
|
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) {
|
func (repo *TokenVerifierRepo) VerifierClientID(ctx context.Context, appName string) (_ string, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
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
|
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
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
|
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
|
||||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||||
"github.com/caos/zitadel/internal/config/types"
|
"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}),
|
handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es}),
|
||||||
newOrg(
|
newOrg(
|
||||||
handler{view, bulkLimit, configs.cycleDuration("Org"), errorCount, es}),
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/caos/zitadel/internal/api/authz"
|
"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/config/types"
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
@@ -49,13 +50,14 @@ type Commands struct {
|
|||||||
keyAlgorithm crypto.EncryptionAlgorithm
|
keyAlgorithm crypto.EncryptionAlgorithm
|
||||||
privateKeyLifetime time.Duration
|
privateKeyLifetime time.Duration
|
||||||
publicKeyLifetime time.Duration
|
publicKeyLifetime time.Duration
|
||||||
|
tokenVerifier *authz.TokenVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Eventstore types.SQLUser
|
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{
|
repo = &Commands{
|
||||||
eventstore: eventstore,
|
eventstore: eventstore,
|
||||||
idGenerator: id.SonyFlakeGenerator,
|
idGenerator: id.SonyFlakeGenerator,
|
||||||
@@ -119,6 +121,8 @@ func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
repo.keyAlgorithm = keyAlgorithm
|
repo.keyAlgorithm = keyAlgorithm
|
||||||
|
|
||||||
|
repo.tokenVerifier = authz.Start(authZRepo)
|
||||||
return repo, nil
|
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,
|
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")
|
return domain.MultiFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "IAM-5m9fs", "Errors.IAM.LoginPolicy.MFA.Unspecified")
|
||||||
}
|
}
|
||||||
multiFactorModel := NewIAMMultiFactorWriteModel(multiFactor)
|
multiFactorModel := NewIAMMultiFactorWriteModel(multiFactor)
|
||||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
|
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
|
||||||
event, err := c.addMultiFactorToDefaultLoginPolicy(ctx, iamAgg, multiFactorModel, multiFactor)
|
event, err := c.addMultiFactorToDefaultLoginPolicy(ctx, iamAgg, multiFactorModel, multiFactor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.MultiFactorTypeUnspecified, nil, err
|
return domain.MultiFactorTypeUnspecified, nil, err
|
||||||
@@ -235,7 +235,7 @@ func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.MultiFactorTypeUnspecified, nil, err
|
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) {
|
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 {
|
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
|
||||||
return nil, caos_errs.ThrowNotFound(nil, "IAM-3M9df", "Errors.IAM.LoginPolicy.MFA.NotExisting")
|
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))
|
pushedEvents, err := c.eventstore.PushEvents(ctx, iam_repo.NewLoginPolicyMultiFactorRemovedEvent(ctx, iamAgg, multiFactor))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@@ -51,12 +51,12 @@ func (wm *IAMSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IAMMultiFactorWriteModel struct {
|
type IAMMultiFactorWriteModel struct {
|
||||||
MultiFactoryWriteModel
|
MultiFactorWriteModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAMMultiFactorWriteModel(factorType domain.MultiFactorType) *IAMMultiFactorWriteModel {
|
func NewIAMMultiFactorWriteModel(factorType domain.MultiFactorType) *IAMMultiFactorWriteModel {
|
||||||
return &IAMMultiFactorWriteModel{
|
return &IAMMultiFactorWriteModel{
|
||||||
MultiFactoryWriteModel{
|
MultiFactorWriteModel{
|
||||||
WriteModel: eventstore.WriteModel{
|
WriteModel: eventstore.WriteModel{
|
||||||
AggregateID: domain.IAMID,
|
AggregateID: domain.IAMID,
|
||||||
ResourceOwner: domain.IAMID,
|
ResourceOwner: domain.IAMID,
|
||||||
@@ -82,7 +82,7 @@ func (wm *IAMMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wm *IAMMultiFactorWriteModel) Reduce() error {
|
func (wm *IAMMultiFactorWriteModel) Reduce() error {
|
||||||
return wm.MultiFactoryWriteModel.Reduce()
|
return wm.MultiFactorWriteModel.Reduce()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *IAMMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
func (wm *IAMMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
@@ -2,7 +2,14 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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/crypto"
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||||
"github.com/caos/zitadel/internal/eventstore/repository/mock"
|
"github.com/caos/zitadel/internal/eventstore/repository/mock"
|
||||||
@@ -12,9 +19,6 @@ import (
|
|||||||
proj_repo "github.com/caos/zitadel/internal/repository/project"
|
proj_repo "github.com/caos/zitadel/internal/repository/project"
|
||||||
usr_repo "github.com/caos/zitadel/internal/repository/user"
|
usr_repo "github.com/caos/zitadel/internal/repository/user"
|
||||||
"github.com/caos/zitadel/internal/repository/usergrant"
|
"github.com/caos/zitadel/internal/repository/usergrant"
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type expect func(mockRepository *mock.MockRepository)
|
type expect func(mockRepository *mock.MockRepository)
|
||||||
@@ -172,3 +176,48 @@ func GetMockSecretGenerator(t *testing.T) crypto.Generator {
|
|||||||
|
|
||||||
return 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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
"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")
|
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)
|
orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel)
|
||||||
pushedEvents, err := c.eventstore.PushEvents(
|
pushedEvents, err := c.eventstore.PushEvents(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -43,6 +52,15 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
|
|||||||
return writeModelToLoginPolicy(&addedPolicy.LoginPolicyWriteModel), nil
|
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) {
|
func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) {
|
||||||
if resourceOwner == "" {
|
if resourceOwner == "" {
|
||||||
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing")
|
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 {
|
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
|
||||||
return nil, caos_errs.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound")
|
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)
|
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel)
|
||||||
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType)
|
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType)
|
||||||
if !hasChanged {
|
if !hasChanged {
|
||||||
@@ -72,6 +96,30 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
|
|||||||
return writeModelToLoginPolicy(&existingPolicy.LoginPolicyWriteModel), nil
|
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) {
|
func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) {
|
||||||
if orgID == "" {
|
if orgID == "" {
|
||||||
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-55Mg9", "Errors.ResourceOwnerMissing")
|
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 {
|
if err != nil {
|
||||||
return domain.MultiFactorTypeUnspecified, nil, err
|
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) {
|
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 {
|
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
|
||||||
return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LoginPolicy.MFA.NotExisting")
|
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))
|
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicyMultiFactorRemovedEvent(ctx, orgAgg, multiFactor))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -51,12 +51,12 @@ func (wm *OrgSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OrgMultiFactorWriteModel struct {
|
type OrgMultiFactorWriteModel struct {
|
||||||
MultiFactoryWriteModel
|
MultiFactorWriteModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOrgMultiFactorWriteModel(orgID string, factorType domain.MultiFactorType) *OrgMultiFactorWriteModel {
|
func NewOrgMultiFactorWriteModel(orgID string, factorType domain.MultiFactorType) *OrgMultiFactorWriteModel {
|
||||||
return &OrgMultiFactorWriteModel{
|
return &OrgMultiFactorWriteModel{
|
||||||
MultiFactoryWriteModel{
|
MultiFactorWriteModel{
|
||||||
WriteModel: eventstore.WriteModel{
|
WriteModel: eventstore.WriteModel{
|
||||||
AggregateID: orgID,
|
AggregateID: orgID,
|
||||||
ResourceOwner: orgID,
|
ResourceOwner: orgID,
|
||||||
@@ -82,7 +82,7 @@ func (wm *OrgMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wm *OrgMultiFactorWriteModel) Reduce() error {
|
func (wm *OrgMultiFactorWriteModel) Reduce() error {
|
||||||
return wm.MultiFactoryWriteModel.Reduce()
|
return wm.MultiFactorWriteModel.Reduce()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *OrgMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
func (wm *OrgMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
@@ -6,11 +6,13 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
"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/org"
|
||||||
"github.com/caos/zitadel/internal/repository/policy"
|
"github.com/caos/zitadel/internal/repository/policy"
|
||||||
"github.com/caos/zitadel/internal/repository/user"
|
"github.com/caos/zitadel/internal/repository/user"
|
||||||
@@ -18,7 +20,8 @@ import (
|
|||||||
|
|
||||||
func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
|
tokenVerifier *authz.TokenVerifier
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -88,12 +91,60 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
|||||||
err: caos_errs.IsErrorAlreadyExists,
|
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",
|
name: "add policy,ok",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
expectFilter(),
|
expectFilter(),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
iam.NewLoginPolicyAddedEvent(context.Background(),
|
||||||
|
&iam.NewAggregate().Aggregate,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
domain.PasswordlessTypeAllowed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
expectPush(
|
expectPush(
|
||||||
[]*repository.Event{
|
[]*repository.Event{
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
@@ -109,6 +160,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@@ -139,7 +191,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore,
|
eventstore: tt.fields.eventstore,
|
||||||
|
tokenVerifier: tt.fields.tokenVerifier,
|
||||||
}
|
}
|
||||||
got, err := r.AddLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
|
got, err := r.AddLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
@@ -157,7 +210,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
|
|||||||
|
|
||||||
func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
|
tokenVerifier *authz.TokenVerifier
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -218,6 +272,53 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
|||||||
err: caos_errs.IsNotFound,
|
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",
|
name: "no changes, precondition error",
|
||||||
fields: fields{
|
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{
|
args: args{
|
||||||
ctx: context.Background(),
|
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(
|
expectPush(
|
||||||
[]*repository.Event{
|
[]*repository.Event{
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
@@ -277,6 +403,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@@ -307,7 +434,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore,
|
eventstore: tt.fields.eventstore,
|
||||||
|
tokenVerifier: tt.fields.tokenVerifier,
|
||||||
}
|
}
|
||||||
got, err := r.ChangeLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
|
got, err := r.ChangeLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
|
@@ -26,13 +26,13 @@ func (wm *SecondFactorWriteModel) Reduce() error {
|
|||||||
return wm.WriteModel.Reduce()
|
return wm.WriteModel.Reduce()
|
||||||
}
|
}
|
||||||
|
|
||||||
type MultiFactoryWriteModel struct {
|
type MultiFactorWriteModel struct {
|
||||||
eventstore.WriteModel
|
eventstore.WriteModel
|
||||||
MFAType domain.MultiFactorType
|
MFAType domain.MultiFactorType
|
||||||
State domain.FactorState
|
State domain.FactorState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *MultiFactoryWriteModel) Reduce() error {
|
func (wm *MultiFactorWriteModel) Reduce() error {
|
||||||
for _, event := range wm.Events {
|
for _, event := range wm.Events {
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
case *policy.MultiFactorAddedEvent:
|
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 {
|
func (c *Commands) SetupStep9(ctx context.Context, step *Step9) error {
|
||||||
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
|
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
|
||||||
multiFactorModel := NewIAMMultiFactorWriteModel(domain.MultiFactorTypeU2FWithPIN)
|
multiFactorModel := NewIAMMultiFactorWriteModel(domain.MultiFactorTypeU2FWithPIN)
|
||||||
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
|
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
|
||||||
if !step.Passwordless {
|
if !step.Passwordless {
|
||||||
return []eventstore.EventPusher{}, nil
|
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
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/caos/zitadel/internal/api/authz"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@@ -14,6 +14,7 @@ const (
|
|||||||
Step9
|
Step9
|
||||||
Step10
|
Step10
|
||||||
Step11
|
Step11
|
||||||
|
Step12
|
||||||
//StepCount marks the the length of possible steps (StepCount-1 == last possible step)
|
//StepCount marks the the length of possible steps (StepCount-1 == last possible step)
|
||||||
StepCount
|
StepCount
|
||||||
)
|
)
|
||||||
|
@@ -181,6 +181,8 @@ func getField(field es_models.Field) string {
|
|||||||
return "editor_user"
|
return "editor_user"
|
||||||
case es_models.Field_EventType:
|
case es_models.Field_EventType:
|
||||||
return "event_type"
|
return "event_type"
|
||||||
|
case es_models.Field_CreationDate:
|
||||||
|
return "creation_date"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@@ -10,4 +10,5 @@ const (
|
|||||||
Field_EditorService
|
Field_EditorService
|
||||||
Field_EditorUser
|
Field_EditorUser
|
||||||
Field_EventType
|
Field_EventType
|
||||||
|
Field_CreationDate
|
||||||
)
|
)
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
)
|
)
|
||||||
@@ -15,6 +17,7 @@ type SearchQueryFactory struct {
|
|||||||
sequenceTo uint64
|
sequenceTo uint64
|
||||||
eventTypes []EventType
|
eventTypes []EventType
|
||||||
resourceOwner string
|
resourceOwner string
|
||||||
|
creationDate time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type searchQuery struct {
|
type searchQuery struct {
|
||||||
@@ -63,7 +66,8 @@ func FactoryFromSearchQuery(query *SearchQuery) *SearchQueryFactory {
|
|||||||
factory = factory.EventTypes(filter.value.([]EventType)...)
|
factory = factory.EventTypes(filter.value.([]EventType)...)
|
||||||
case Field_EditorService, Field_EditorUser:
|
case Field_EditorService, Field_EditorUser:
|
||||||
logging.Log("MODEL-Mr0VN").WithField("value", filter.value).Panic("field not converted to factory")
|
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
|
return factory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (factory *SearchQueryFactory) CreationDateNewer(time time.Time) *SearchQueryFactory {
|
||||||
|
factory.creationDate = time
|
||||||
|
return factory
|
||||||
|
}
|
||||||
|
|
||||||
func (factory *SearchQueryFactory) OrderDesc() *SearchQueryFactory {
|
func (factory *SearchQueryFactory) OrderDesc() *SearchQueryFactory {
|
||||||
factory.desc = true
|
factory.desc = true
|
||||||
return factory
|
return factory
|
||||||
@@ -142,6 +151,7 @@ func (factory *SearchQueryFactory) Build() (*searchQuery, error) {
|
|||||||
factory.sequenceToFilter,
|
factory.sequenceToFilter,
|
||||||
factory.eventTypeFilter,
|
factory.eventTypeFilter,
|
||||||
factory.resourceOwnerFilter,
|
factory.resourceOwnerFilter,
|
||||||
|
factory.creationDateNewerFilter,
|
||||||
} {
|
} {
|
||||||
if filter := f(); filter != nil {
|
if filter := f(); filter != nil {
|
||||||
filters = append(filters, filter)
|
filters = append(filters, filter)
|
||||||
@@ -211,3 +221,10 @@ func (factory *SearchQueryFactory) resourceOwnerFilter() *Filter {
|
|||||||
}
|
}
|
||||||
return NewFilter(Field_ResourceOwner, factory.resourceOwner, Operation_Equals)
|
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
|
package models
|
||||||
|
|
||||||
import "github.com/caos/zitadel/internal/errors"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
//SearchQuery is deprecated. Use SearchQueryFactory
|
//SearchQuery is deprecated. Use SearchQueryFactory
|
||||||
type SearchQuery struct {
|
type SearchQuery struct {
|
||||||
@@ -68,6 +72,10 @@ func (q *SearchQuery) ResourceOwnerFilter(resourceOwner string) *SearchQuery {
|
|||||||
return q.setFilter(NewFilter(Field_ResourceOwner, resourceOwner, Operation_Equals))
|
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 {
|
func (q *SearchQuery) setFilter(filter *Filter) *SearchQuery {
|
||||||
for i, f := range q.Filters {
|
for i, f := range q.Filters {
|
||||||
if f.field == filter.field && f.field != Field_LatestSequence {
|
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/caos/zitadel/internal/user/repository/view"
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
"github.com/caos/zitadel/internal/api/authz"
|
"github.com/caos/zitadel/internal/api/authz"
|
||||||
@@ -93,8 +94,8 @@ func (repo *OrgRepository) SearchMyOrgDomains(ctx context.Context, request *org_
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *OrgRepository) OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*org_model.OrgChanges, error) {
|
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)
|
changes, err := repo.getOrgChanges(ctx, id, lastSequence, limit, sortAscending, auditLogRetention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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) {
|
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)
|
query := org_view.ChangesQuery(orgID, lastSequence, limit, sortAscending, auditLogRetention)
|
||||||
|
|
||||||
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
|
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||||
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
@@ -180,8 +181,8 @@ func (repo *ProjectRepo) SearchProjectRoles(ctx context.Context, projectID strin
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *ProjectRepo) ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ProjectChanges, error) {
|
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)
|
changes, err := repo.getProjectChanges(ctx, id, lastSequence, limit, sortAscending, retention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -254,8 +255,8 @@ func (repo *ProjectRepo) SearchApplications(ctx context.Context, request *proj_m
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *ProjectRepo) ApplicationChanges(ctx context.Context, id string, appId string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ApplicationChanges, error) {
|
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, id, appId, lastSequence, limit, sortAscending)
|
changes, err := repo.getApplicationChanges(ctx, projectID, appID, lastSequence, limit, sortAscending, retention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -505,8 +506,8 @@ func (r *ProjectRepo) getUserEvents(ctx context.Context, userID string, sequence
|
|||||||
return r.Eventstore.FilterEvents(ctx, query)
|
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) {
|
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)
|
query := proj_view.ChangesQuery(id, lastSequence, limit, sortAscending, retention)
|
||||||
|
|
||||||
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
|
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -561,8 +562,8 @@ func (repo *ProjectRepo) getProjectEvents(ctx context.Context, id string, sequen
|
|||||||
return repo.Eventstore.FilterEvents(ctx, query)
|
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) {
|
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)
|
query := proj_view.ChangesQuery(projectID, lastSequence, limit, sortAscending, retention)
|
||||||
|
|
||||||
events, err := repo.Eventstore.FilterEvents(ctx, query)
|
events, err := repo.Eventstore.FilterEvents(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -2,6 +2,8 @@ package eventstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/domain"
|
"github.com/caos/zitadel/internal/domain"
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
"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)
|
return repo.View.UserIDsByDomain(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*usr_model.UserChanges, error) {
|
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)
|
changes, err := repo.getUserChanges(ctx, id, lastSequence, limit, sortAscending, retention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -280,8 +282,8 @@ func (repo *UserRepo) SearchUserMemberships(ctx context.Context, request *usr_mo
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool) (*usr_model.UserChanges, error) {
|
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)
|
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention)
|
||||||
|
|
||||||
events, err := r.Eventstore.FilterEvents(ctx, query)
|
events, err := r.Eventstore.FilterEvents(ctx, query)
|
||||||
if err != nil {
|
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
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||||
"github.com/caos/zitadel/internal/config/types"
|
"github.com/caos/zitadel/internal/config/types"
|
||||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
"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}),
|
handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}),
|
||||||
newMailText(
|
newMailText(
|
||||||
handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}),
|
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.UserRepo
|
||||||
eventstore.UserGrantRepo
|
eventstore.UserGrantRepo
|
||||||
eventstore.IAMRepository
|
eventstore.IAMRepository
|
||||||
|
eventstore.FeaturesRepo
|
||||||
view *mgmt_view.View
|
view *mgmt_view.View
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +55,9 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie
|
|||||||
ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID},
|
ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID},
|
||||||
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults},
|
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults},
|
||||||
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, view},
|
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, view},
|
||||||
IAMRepository: eventstore.IAMRepository{
|
IAMRepository: eventstore.IAMRepository{IAMV2Query: queries},
|
||||||
IAMV2Query: queries,
|
FeaturesRepo: eventstore.FeaturesRepo{es, view, conf.SearchLimit, systemDefaults},
|
||||||
},
|
view: view,
|
||||||
view: view,
|
|
||||||
}, nil
|
}, 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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
type OrgRepository interface {
|
type OrgRepository interface {
|
||||||
OrgByID(ctx context.Context, id string) (*org_model.OrgView, error)
|
OrgByID(ctx context.Context, id string) (*org_model.OrgView, error)
|
||||||
OrgByDomainGlobal(ctx context.Context, domain 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)
|
SearchMyOrgDomains(ctx context.Context, request *org_model.OrgDomainSearchRequest) (*org_model.OrgDomainSearchResponse, error)
|
||||||
|
|
||||||
|
@@ -2,6 +2,8 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||||
|
|
||||||
key_model "github.com/caos/zitadel/internal/key/model"
|
key_model "github.com/caos/zitadel/internal/key/model"
|
||||||
@@ -22,11 +24,11 @@ type ProjectRepository interface {
|
|||||||
GetProjectMemberRoles(ctx context.Context) ([]string, error)
|
GetProjectMemberRoles(ctx context.Context) ([]string, error)
|
||||||
|
|
||||||
SearchProjectRoles(ctx context.Context, projectId string, request *model.ProjectRoleSearchRequest) (*model.ProjectRoleSearchResponse, 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)
|
ApplicationByID(ctx context.Context, projectID, appID string) (*model.ApplicationView, error)
|
||||||
SearchApplications(ctx context.Context, request *model.ApplicationSearchRequest) (*model.ApplicationSearchResponse, 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)
|
SearchClientKeys(ctx context.Context, request *key_model.AuthNKeySearchRequest) (*key_model.AuthNKeySearchResponse, error)
|
||||||
GetClientKey(ctx context.Context, projectID, applicationID, keyID string) (*key_model.AuthNKeyView, error)
|
GetClientKey(ctx context.Context, projectID, applicationID, keyID string) (*key_model.AuthNKeyView, error)
|
||||||
|
|
||||||
|
@@ -7,4 +7,5 @@ type Repository interface {
|
|||||||
UserRepository
|
UserRepository
|
||||||
UserGrantRepository
|
UserGrantRepository
|
||||||
IamRepository
|
IamRepository
|
||||||
|
FeaturesRepository
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
key_model "github.com/caos/zitadel/internal/key/model"
|
key_model "github.com/caos/zitadel/internal/key/model"
|
||||||
"github.com/caos/zitadel/internal/user/model"
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
@@ -15,7 +16,7 @@ type UserRepository interface {
|
|||||||
GetUserByLoginNameGlobal(ctx context.Context, email string) (*model.UserView, error)
|
GetUserByLoginNameGlobal(ctx context.Context, email string) (*model.UserView, error)
|
||||||
IsUserUnique(ctx context.Context, userName, email string) (bool, 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)
|
ProfileByID(ctx context.Context, userID string) (*model.Profile, error)
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||||
@@ -36,13 +38,16 @@ func OrgNameUniqueQuery(name string) *es_models.SearchQuery {
|
|||||||
SetLimit(1)
|
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().
|
query := es_models.NewSearchQuery().
|
||||||
AggregateTypeFilter(model.OrgAggregate)
|
AggregateTypeFilter(model.OrgAggregate)
|
||||||
|
|
||||||
if !sortAscending {
|
if !sortAscending {
|
||||||
query.OrderDesc()
|
query.OrderDesc()
|
||||||
}
|
}
|
||||||
|
if auditLogRetention > 0 {
|
||||||
|
query.CreationDateNewerFilter(time.Now().Add(-auditLogRetention))
|
||||||
|
}
|
||||||
|
|
||||||
query.LatestSequenceFilter(latestSequence).
|
query.LatestSequenceFilter(latestSequence).
|
||||||
AggregateIDFilter(orgID).
|
AggregateIDFilter(orgID).
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
"github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
"github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
||||||
@@ -20,12 +22,15 @@ func ProjectQuery(latestSequence uint64) *es_models.SearchQuery {
|
|||||||
LatestSequenceFilter(latestSequence)
|
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().
|
query := es_models.NewSearchQuery().
|
||||||
AggregateTypeFilter(model.ProjectAggregate)
|
AggregateTypeFilter(model.ProjectAggregate)
|
||||||
if !sortAscending {
|
if !sortAscending {
|
||||||
query.OrderDesc()
|
query.OrderDesc()
|
||||||
}
|
}
|
||||||
|
if retention > 0 {
|
||||||
|
query.CreationDateNewerFilter(time.Now().Add(-retention))
|
||||||
|
}
|
||||||
|
|
||||||
query.LatestSequenceFilter(latestSequence).
|
query.LatestSequenceFilter(latestSequence).
|
||||||
AggregateIDFilter(projectID).
|
AggregateIDFilter(projectID).
|
||||||
|
@@ -14,4 +14,5 @@ extend google.protobuf.MethodOptions {
|
|||||||
message AuthOption {
|
message AuthOption {
|
||||||
string permission = 1;
|
string permission = 1;
|
||||||
string check_field_name = 2;
|
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 {
|
var {{$s.Name}}_AuthMethods = authz.MethodMapping {
|
||||||
{{ range $m := $s.Method}}
|
{{ range $m := $s.Method}}
|
||||||
{{ $mAuthOpt := option $m.Options "zitadel.v1.auth_option" }}
|
{{ $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{
|
"/{{$.File.Package}}.{{$s.Name}}/{{.Name}}": authz.Option{
|
||||||
Permission: "{{$mAuthOpt.Permission}}",
|
Permission: "{{$mAuthOpt.Permission}}",
|
||||||
CheckParam: "{{$mAuthOpt.CheckFieldName}}",
|
CheckParam: "{{$mAuthOpt.CheckFieldName}}",
|
||||||
|
Feature: "{{$mAuthOpt.Feature}}",
|
||||||
},
|
},
|
||||||
{{end}}
|
{{end}}
|
||||||
{{ 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(MailTemplateAddedEventType, MailTemplateAddedEventMapper).
|
||||||
RegisterFilterEventMapper(MailTemplateChangedEventType, MailTemplateChangedEventMapper).
|
RegisterFilterEventMapper(MailTemplateChangedEventType, MailTemplateChangedEventMapper).
|
||||||
RegisterFilterEventMapper(MailTextAddedEventType, MailTextAddedEventMapper).
|
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(IDPConfigDeactivatedEventType, IDPConfigDeactivatedEventMapper).
|
||||||
RegisterFilterEventMapper(IDPConfigReactivatedEventType, IDPConfigReactivatedEventMapper).
|
RegisterFilterEventMapper(IDPConfigReactivatedEventType, IDPConfigReactivatedEventMapper).
|
||||||
RegisterFilterEventMapper(IDPOIDCConfigAddedEventType, IDPOIDCConfigAddedEventMapper).
|
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
|
Step9 *command.Step9
|
||||||
Step10 *command.Step10
|
Step10 *command.Step10
|
||||||
Step11 *command.Step11
|
Step11 *command.Step11
|
||||||
|
Step12 *command.Step12
|
||||||
}
|
}
|
||||||
|
|
||||||
func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) {
|
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.Step9,
|
||||||
setup.Step10,
|
setup.Step10,
|
||||||
setup.Step11,
|
setup.Step11,
|
||||||
|
setup.Step12,
|
||||||
} {
|
} {
|
||||||
if step.Step() <= currentDone {
|
if step.Step() <= currentDone {
|
||||||
continue
|
continue
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||||
@@ -20,12 +22,15 @@ func UserQuery(latestSequence uint64) *es_models.SearchQuery {
|
|||||||
LatestSequenceFilter(latestSequence)
|
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().
|
query := es_models.NewSearchQuery().
|
||||||
AggregateTypeFilter(model.UserAggregate)
|
AggregateTypeFilter(model.UserAggregate)
|
||||||
if !sortAscending {
|
if !sortAscending {
|
||||||
query.OrderDesc()
|
query.OrderDesc()
|
||||||
}
|
}
|
||||||
|
if retention > 0 {
|
||||||
|
query.CreationDateNewerFilter(time.Now().Add(-retention))
|
||||||
|
}
|
||||||
|
|
||||||
query.LatestSequenceFilter(latestSequence).
|
query.LatestSequenceFilter(latestSequence).
|
||||||
AggregateIDFilter(userID).
|
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/org.proto";
|
||||||
import "zitadel/policy.proto";
|
import "zitadel/policy.proto";
|
||||||
import "zitadel/member.proto";
|
import "zitadel/member.proto";
|
||||||
|
import "zitadel/features.proto";
|
||||||
|
|
||||||
import "google/api/annotations.proto";
|
import "google/api/annotations.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "google/protobuf/duration.proto";
|
||||||
|
|
||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ service AdminService {
|
|||||||
get: "/healthz"
|
get: "/healthz"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc IsOrgUnique(IsOrgUniqueRequest) returns (IsOrgUniqueResponse) {
|
rpc IsOrgUnique(IsOrgUniqueRequest) returns (IsOrgUniqueResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/orgs/_is_unique"
|
get: "/orgs/_is_unique"
|
||||||
@@ -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) {
|
rpc GetOrgIAMPolicy(GetOrgIAMPolicyRequest) returns (GetOrgIAMPolicyResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/policies/orgiam"
|
get: "/policies/orgiam"
|
||||||
@@ -259,18 +313,18 @@ service AdminService {
|
|||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/policies/login"
|
get: "/policies/login"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "iam.policy.read"
|
permission: "iam.policy.read"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc UpdateLoginPolicy(UpdateLoginPolicyRequest) returns (UpdateLoginPolicyResponse) {
|
rpc UpdateLoginPolicy(UpdateLoginPolicyRequest) returns (UpdateLoginPolicyResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
put: "/policies/login"
|
put: "/policies/login"
|
||||||
body: "*"
|
body: "*"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "iam.policy.write"
|
permission: "iam.policy.write"
|
||||||
};
|
};
|
||||||
@@ -395,18 +449,18 @@ service AdminService {
|
|||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/policies/password/age"
|
get: "/policies/password/age"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "iam.policy.read"
|
permission: "iam.policy.read"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc UpdatePasswordAgePolicy(UpdatePasswordAgePolicyRequest) returns (UpdatePasswordAgePolicyResponse) {
|
rpc UpdatePasswordAgePolicy(UpdatePasswordAgePolicyRequest) returns (UpdatePasswordAgePolicyResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
put: "/policies/password/age"
|
put: "/policies/password/age"
|
||||||
body: "*"
|
body: "*"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "iam.policy.write"
|
permission: "iam.policy.write"
|
||||||
};
|
};
|
||||||
@@ -416,18 +470,18 @@ service AdminService {
|
|||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/policies/password/lockout"
|
get: "/policies/password/lockout"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "iam.policy.read"
|
permission: "iam.policy.read"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc UpdatePasswordLockoutPolicy(UpdatePasswordLockoutPolicyRequest) returns (UpdatePasswordLockoutPolicyResponse) {
|
rpc UpdatePasswordLockoutPolicy(UpdatePasswordLockoutPolicyRequest) returns (UpdatePasswordLockoutPolicyResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
put: "/policies/password/lockout"
|
put: "/policies/password/lockout"
|
||||||
body: "*"
|
body: "*"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "iam.policy.write"
|
permission: "iam.policy.write"
|
||||||
};
|
};
|
||||||
@@ -583,9 +637,9 @@ message SetUpOrgRequest {
|
|||||||
string phone = 1 [(validate.rules).string = {min_len: 1, max_len: 50, prefix: "+"}];
|
string phone = 1 [(validate.rules).string = {min_len: 1, max_len: 50, prefix: "+"}];
|
||||||
bool is_phone_verified = 2;
|
bool is_phone_verified = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
|
||||||
Profile profile = 2 [(validate.rules).message.required = true];
|
Profile profile = 2 [(validate.rules).message.required = true];
|
||||||
Email email = 3 [(validate.rules).message.required = true];
|
Email email = 3 [(validate.rules).message.required = true];
|
||||||
Phone phone = 4;
|
Phone phone = 4;
|
||||||
@@ -697,6 +751,66 @@ message UpdateIDPOIDCConfigResponse {
|
|||||||
zitadel.v1.ObjectDetails details = 1;
|
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 GetOrgIAMPolicyRequest {}
|
||||||
|
|
||||||
message GetOrgIAMPolicyResponse {
|
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) {
|
rpc ListMyZitadelPermissions(ListMyZitadelPermissionsRequest) returns (ListMyZitadelPermissionsResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/permissions/zitadel/me/_search"
|
post: "/permissions/zitadel/me/_search"
|
||||||
@@ -658,6 +668,12 @@ message ListMyProjectOrgsResponse {
|
|||||||
repeated zitadel.org.v1.Org result = 2;
|
repeated zitadel.org.v1.Org result = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ListMyZitadelFeaturesRequest {}
|
||||||
|
|
||||||
|
message ListMyZitadelFeaturesResponse {
|
||||||
|
repeated string result = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ListMyZitadelPermissionsRequest {}
|
message ListMyZitadelPermissionsRequest {}
|
||||||
|
|
||||||
message ListMyZitadelPermissionsResponse {
|
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/message.proto";
|
||||||
import "zitadel/change.proto";
|
import "zitadel/change.proto";
|
||||||
import "zitadel/auth_n_key.proto";
|
import "zitadel/auth_n_key.proto";
|
||||||
|
import "zitadel/features.proto";
|
||||||
|
|
||||||
import "google/api/annotations.proto";
|
import "google/api/annotations.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
@@ -44,7 +45,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
|||||||
schemes: HTTPS;
|
schemes: HTTPS;
|
||||||
consumes: "application/json";
|
consumes: "application/json";
|
||||||
produces: "application/json";
|
produces: "application/json";
|
||||||
|
|
||||||
consumes: "application/grpc";
|
consumes: "application/grpc";
|
||||||
produces: "application/grpc";
|
produces: "application/grpc";
|
||||||
|
|
||||||
@@ -242,7 +243,7 @@ service ManagementService {
|
|||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/users/{user_id}/username"
|
get: "/users/{user_id}/username"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "user.write"
|
permission: "user.write"
|
||||||
};
|
};
|
||||||
@@ -292,7 +293,7 @@ service ManagementService {
|
|||||||
|
|
||||||
rpc ResendHumanInitialization(ResendHumanInitializationRequest) returns (ResendHumanInitializationResponse) {
|
rpc ResendHumanInitialization(ResendHumanInitializationRequest) returns (ResendHumanInitializationResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/users/{user_id}/_resend_initialization"
|
post: "/users/{user_id}/_resend_initialization"
|
||||||
body: "*"
|
body: "*"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ service ManagementService {
|
|||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "user.write"
|
permission: "user.write"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc GetHumanPhone(GetHumanPhoneRequest) returns (GetHumanPhoneResponse) {
|
rpc GetHumanPhone(GetHumanPhoneRequest) returns (GetHumanPhoneResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
@@ -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) {
|
rpc GetOrgIAMPolicy(GetOrgIAMPolicyRequest) returns (GetOrgIAMPolicyResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/policies/orgiam"
|
get: "/policies/orgiam"
|
||||||
@@ -1378,6 +1389,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1389,6 +1401,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1421,6 +1434,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,6 +1445,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1452,6 +1467,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy.factors"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1462,6 +1478,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy.factors"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1483,6 +1500,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy.factors"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1493,6 +1511,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "login_policy.factors"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,6 +1543,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "password_complexity_policy"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1535,6 +1555,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "password_complexity_policy"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1680,6 +1701,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "label_policy"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1691,6 +1713,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "policy.write"
|
permission: "policy.write"
|
||||||
|
feature: "label_policy"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1733,6 +1756,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "org.idp.write"
|
permission: "org.idp.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1744,6 +1768,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "org.idp.write"
|
permission: "org.idp.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,6 +1780,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "org.idp.write"
|
permission: "org.idp.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,6 +1791,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "org.idp.write"
|
permission: "org.idp.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1776,6 +1803,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "org.idp.write"
|
permission: "org.idp.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1787,6 +1815,7 @@ service ManagementService {
|
|||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "org.idp.write"
|
permission: "org.idp.write"
|
||||||
|
feature: "login_policy.idp"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2956,6 +2985,12 @@ message BulkRemoveUserGrantRequest {
|
|||||||
|
|
||||||
message BulkRemoveUserGrantResponse {}
|
message BulkRemoveUserGrantResponse {}
|
||||||
|
|
||||||
|
message GetFeaturesRequest {}
|
||||||
|
|
||||||
|
message GetFeaturesResponse {
|
||||||
|
zitadel.features.v1.Features features = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetOrgIAMPolicyRequest {}
|
message GetOrgIAMPolicyRequest {}
|
||||||
|
|
||||||
message GetOrgIAMPolicyResponse {
|
message GetOrgIAMPolicyResponse {
|
||||||
|
@@ -14,4 +14,5 @@ extend google.protobuf.MethodOptions {
|
|||||||
message AuthOption {
|
message AuthOption {
|
||||||
string permission = 1;
|
string permission = 1;
|
||||||
string check_field_name = 2;
|
string check_field_name = 2;
|
||||||
}
|
string feature = 3;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user