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:
Livio Amstutz
2021-03-25 17:26:21 +01:00
committed by GitHub
parent c9b3839f3d
commit a4763b1e4c
97 changed files with 3335 additions and 109 deletions

View File

@@ -4,6 +4,8 @@ InternalAuthZ:
Permissions:
- "iam.read"
- "iam.write"
- "iam.features.read"
- "iam.features.write"
- "iam.policy.read"
- "iam.policy.write"
- "iam.policy.delete"
@@ -31,6 +33,7 @@ InternalAuthZ:
- "user.grant.write"
- "user.grant.delete"
- "user.membership.read"
- "features.read"
- "policy.read"
- "policy.write"
- "policy.delete"
@@ -56,6 +59,7 @@ InternalAuthZ:
- Role: 'IAM_OWNER_VIEWER'
Permissions:
- "iam.read"
- "iam.features.read"
- "iam.policy.read"
- "iam.member.read"
- "iam.idp.read"
@@ -66,6 +70,7 @@ InternalAuthZ:
- "user.global.read"
- "user.grant.read"
- "user.membership.read"
- "features.read"
- "policy.read"
- "project.read"
- "project.member.read"
@@ -93,6 +98,7 @@ InternalAuthZ:
- "user.grant.write"
- "user.grant.delete"
- "user.membership.read"
- "features.read"
- "policy.read"
- "policy.write"
- "policy.delete"
@@ -123,6 +129,7 @@ InternalAuthZ:
- "user.global.read"
- "user.grant.read"
- "user.membership.read"
- "features.read"
- "policy.read"
- "project.read"
- "project.member.read"

View File

@@ -103,24 +103,24 @@ func startZitadel(configPaths []string) {
logging.Log("MAIN-FaF2r").OnError(err).Fatal("cannot read config")
ctx := context.Background()
esCommands, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Commands.Eventstore)
if err != nil {
return
}
commands, err := command.StartCommands(esCommands, conf.SystemDefaults, conf.InternalAuthZ)
if err != nil {
return
}
esQueries, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Queries.Eventstore)
if err != nil {
return
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start eventstore for queries")
}
queries, err := query.StartQueries(esQueries, conf.SystemDefaults)
if err != nil {
return
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start queries")
}
authZRepo, err := authz.Start(ctx, conf.AuthZ, conf.InternalAuthZ, conf.SystemDefaults, queries)
logging.Log("MAIN-s9KOw").OnError(err).Fatal("error starting authz repo")
esCommands, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Commands.Eventstore)
if err != nil {
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start eventstore for commands")
}
commands, err := command.StartCommands(esCommands, conf.SystemDefaults, conf.InternalAuthZ, authZRepo)
if err != nil {
logging.Log("MAIN-Ddv21").OnError(err).Fatal("cannot start commands")
}
var authRepo *auth_es.EsRepository
if *authEnabled || *oidcEnabled || *loginEnabled {
authRepo, err = auth_es.Start(conf.Auth, conf.InternalAuthZ, conf.SystemDefaults, commands, queries, authZRepo, esQueries)
@@ -190,7 +190,7 @@ func startSetup(configPaths []string, localDevMode bool) {
es, err := eventstore.Start(conf.Eventstore)
logging.Log("MAIN-Ddt3").OnError(err).Fatal("cannot start eventstore")
commands, err := command.StartCommands(es, conf.SystemDefaults, conf.InternalAuthZ)
commands, err := command.StartCommands(es, conf.SystemDefaults, conf.InternalAuthZ, nil)
logging.Log("MAIN-dsjrr").OnError(err).Fatal("cannot start command side")
err = setup.Execute(ctx, conf.SetUp, conf.SystemDefaults.IamID, commands)

View File

@@ -175,3 +175,6 @@ SetUp:
ButtonText: Login
Step11:
MigrateV1EventstoreToV2: $ZITADEL_MIGRATE_ES_V1
Step12:
TierName: FREE Tier
AuditLogRetention: 9600h #400d = ~13months

View File

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

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

View File

@@ -1,9 +1,10 @@
package handler
import (
"github.com/caos/zitadel/internal/eventstore/v1"
"time"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/admin/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types"
@@ -62,6 +63,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}),
newMailText(
handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}),
newFeatures(
handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
}
}

View File

@@ -24,6 +24,7 @@ type EsRepository struct {
eventstore.OrgRepo
eventstore.IAMRepository
eventstore.AdministratorRepo
eventstore.FeaturesRepo
}
func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, roles []string) (*EsRepository, error) {
@@ -60,6 +61,12 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, r
AdministratorRepo: eventstore.AdministratorRepo{
View: view,
},
FeaturesRepo: eventstore.FeaturesRepo{
Eventstore: es,
View: view,
SearchLimit: conf.SearchLimit,
SystemDefaults: systemDefaults,
},
}, nil
}

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

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

View File

@@ -7,4 +7,5 @@ type Repository interface {
OrgRepository
IAMRepository
AdministratorRepository
FeaturesRepository
}

View File

@@ -23,6 +23,13 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
return nil, err
}
if requiredAuthOption.Feature != "" {
err = CheckOrgFeatures(ctx, verifier, ctxData.OrgID, requiredAuthOption.Feature)
if err != nil {
return nil, err
}
}
if requiredAuthOption.Permission == authenticated {
return func(parent context.Context) context.Context {
return context.WithValue(parent, dataKey, ctxData)
@@ -49,6 +56,10 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
}, nil
}
func CheckOrgFeatures(ctx context.Context, t *TokenVerifier, orgID string, requiredFeatures ...string) error {
return t.authZRepo.CheckOrgFeatures(ctx, orgID, requiredFeatures...)
}
func checkUserPermissions(req interface{}, userPerms []string, authOpt Option) error {
if len(userPerms) == 0 {
return errors.ThrowPermissionDenied(nil, "AUTH-5mWD2", "No matching permissions found")

View File

@@ -14,6 +14,7 @@ type MethodMapping map[string]Option
type Option struct {
Permission string
CheckParam string
Feature string
}
func (a *Config) getPermissionsFromRole(role string) []string {

View File

@@ -34,6 +34,10 @@ func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (st
return "clientID", nil
}
func (v *testVerifier) CheckOrgFeatures(context.Context, string, ...string) error {
return nil
}
func equalStringArray(a, b []string) bool {
if len(a) != len(b) {
return false

View File

@@ -25,6 +25,7 @@ type authZRepo interface {
SearchMyMemberships(ctx context.Context) ([]*Membership, error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, orgID string) error
CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error
}
func Start(authZRepo authZRepo) (v *TokenVerifier) {

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

View File

@@ -1,6 +1,8 @@
package admin
import (
"google.golang.org/grpc"
"github.com/caos/zitadel/internal/admin/repository"
"github.com/caos/zitadel/internal/admin/repository/eventsourcing"
"github.com/caos/zitadel/internal/api/authz"
@@ -8,7 +10,6 @@ import (
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/pkg/grpc/admin"
"google.golang.org/grpc"
)
const (
@@ -25,6 +26,7 @@ type Server struct {
iam repository.IAMRepository
administrator repository.AdministratorRepository
repo repository.Repository
features repository.FeaturesRepository
}
type Config struct {
@@ -39,6 +41,7 @@ func CreateServer(command *command.Commands, query *query.Queries, repo reposito
iam: repo,
administrator: repo,
repo: repo,
features: repo,
}
}

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

View File

@@ -24,7 +24,11 @@ func (s *Server) GetMyUser(ctx context.Context, _ *auth_pb.GetMyUserRequest) (*a
func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserChangesRequest) (*auth_pb.ListMyUserChangesResponse, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
changes, err := s.repo.MyUserChanges(ctx, offset, limit, asc)
features, err := s.repo.GetOrgFeatures(ctx, authz.GetCtxData(ctx).ResourceOwner)
if err != nil {
return nil, err
}
changes, err := s.repo.MyUserChanges(ctx, offset, limit, asc, features.AuditLogRetention)
if err != nil {
return nil, err
}

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

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

View File

@@ -33,7 +33,11 @@ func (s *Server) GetOrgByDomainGlobal(ctx context.Context, req *mgmt_pb.GetOrgBy
func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChangesRequest) (*mgmt_pb.ListOrgChangesResponse, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
response, err := s.org.OrgChanges(ctx, authz.GetCtxData(ctx).OrgID, offset, limit, asc)
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
response, err := s.org.OrgChanges(ctx, authz.GetCtxData(ctx).OrgID, offset, limit, asc, features.AuditLogRetention)
if err != nil {
return nil, err
}

View File

@@ -73,7 +73,11 @@ func (s *Server) ListGrantedProjects(ctx context.Context, req *mgmt_pb.ListGrant
func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjectChangesRequest) (*mgmt_pb.ListProjectChangesResponse, error) {
offset, limit, asc := object_grpc.ListQueryToModel(req.Query)
res, err := s.project.ProjectChanges(ctx, req.ProjectId, offset, limit, asc)
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
res, err := s.project.ProjectChanges(ctx, req.ProjectId, offset, limit, asc, features.AuditLogRetention)
if err != nil {
return nil, err
}

View File

@@ -42,7 +42,11 @@ func (s *Server) ListApps(ctx context.Context, req *mgmt_pb.ListAppsRequest) (*m
func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChangesRequest) (*mgmt_pb.ListAppChangesResponse, error) {
offset, limit, asc := object_grpc.ListQueryToModel(req.Query)
res, err := s.project.ApplicationChanges(ctx, req.ProjectId, req.AppId, offset, limit, asc)
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
res, err := s.project.ApplicationChanges(ctx, req.ProjectId, req.AppId, offset, limit, asc, features.AuditLogRetention)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,8 @@
package management
import (
"google.golang.org/grpc"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/server"
"github.com/caos/zitadel/internal/command"
@@ -9,7 +11,6 @@ import (
"github.com/caos/zitadel/internal/management/repository/eventsourcing"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/pkg/grpc/management"
"google.golang.org/grpc"
)
const (
@@ -27,6 +28,7 @@ type Server struct {
user repository.UserRepository
usergrant repository.UserGrantRepository
iam repository.IamRepository
features repository.FeaturesRepository
authZ authz.Config
systemDefaults systemdefaults.SystemDefaults
}
@@ -44,6 +46,7 @@ func CreateServer(command *command.Commands, query *query.Queries, repo reposito
user: repo,
usergrant: repo,
iam: repo,
features: repo,
systemDefaults: sd,
}
}

View File

@@ -53,7 +53,11 @@ func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) (
func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChangesRequest) (*mgmt_pb.ListUserChangesResponse, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
res, err := s.user.UserChanges(ctx, req.UserId, offset, limit, asc)
features, err := s.features.GetOrgFeatures(ctx, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
res, err := s.user.UserChanges(ctx, req.UserId, offset, limit, asc, features.AuditLogRetention)
if err != nil {
return nil, err
}

View File

@@ -11,7 +11,7 @@ func ModelLoginPolicyToPb(policy *model.LoginPolicyView) *policy_pb.LoginPolicy
IsDefault: policy.Default,
AllowUsernamePassword: policy.AllowUsernamePassword,
AllowRegister: policy.AllowRegister,
AllowExternalIdp: policy.AllowRegister,
AllowExternalIdp: policy.AllowExternalIDP,
ForceMfa: policy.ForceMFA,
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
}

View File

@@ -37,6 +37,9 @@ func (v *verifierMock) ExistsOrg(ctx context.Context, orgID string) error {
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, error) {
return "", nil
}
func (v *verifierMock) CheckOrgFeatures(context.Context, string, ...string) error {
return nil
}
func Test_authorize(t *testing.T) {
type args struct {

View File

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

View File

@@ -2,6 +2,8 @@ package eventstore
import (
"context"
"time"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/golang/protobuf/ptypes"
@@ -187,8 +189,8 @@ func (repo *UserRepo) UserByLoginName(ctx context.Context, loginname string) (*m
}
return usr_view_model.UserToModel(&userCopy), nil
}
func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error) {
changes, err := repo.getUserChanges(ctx, authz.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending)
func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) {
changes, err := repo.getUserChanges(ctx, authz.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending, retention)
if err != nil {
return nil, err
}
@@ -215,8 +217,8 @@ func (repo *UserRepo) MachineKeyByID(ctx context.Context, keyID string) (*key_mo
return key_view_model.AuthNKeyToModel(key), nil
}
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error) {
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending)
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) {
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention)
events, err := r.Eventstore.FilterEvents(ctx, query)
if err != nil {

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

View File

@@ -68,6 +68,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
handler{view, bulkLimit, configs.cycleDuration("OrgIAMPolicy"), errorCount, es}),
newProjectRole(handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount, es}),
newLabelPolicy(handler{view, bulkLimit, configs.cycleDuration("LabelPolicy"), errorCount, es}),
newFeatures(handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
}
}

View File

@@ -42,6 +42,7 @@ type EsRepository struct {
eventstore.UserGrantRepo
eventstore.OrgRepository
eventstore.IAMRepository
eventstore.FeaturesRepo
}
func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, authZRepo *authz_repo.EsRepository, esV2 *es2.Eventstore) (*EsRepository, error) {
@@ -142,6 +143,10 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
IAMID: systemDefaults.IamID,
IAMV2QuerySide: queries,
},
eventstore.FeaturesRepo{
Eventstore: es,
View: view,
},
}, nil
}

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

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

View File

@@ -16,4 +16,5 @@ type Repository interface {
UserGrantRepository
OrgRepository
IAMRepository
FeaturesRepository
}

View File

@@ -2,6 +2,7 @@ package repository
import (
"context"
"time"
key_model "github.com/caos/zitadel/internal/key/model"
@@ -36,7 +37,7 @@ type myUserRepo interface {
GetMyPasswordless(ctx context.Context) ([]*model.WebAuthNView, error)
MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error)
MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error)
SearchMyUserMemberships(ctx context.Context, request *model.UserMembershipSearchRequest) (*model.UserMembershipSearchResponse, error)
}

View File

@@ -2,26 +2,26 @@ package eventstore
import (
"context"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore/v1"
es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
"k8s.io/apimachinery/pkg/api/errors"
"strings"
"time"
"github.com/caos/zitadel/internal/eventstore/v1/models"
usr_view "github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/logging"
"k8s.io/apimachinery/pkg/api/errors"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk"
features_view_model "github.com/caos/zitadel/internal/features/repository/view/model"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/telemetry/tracing"
usr_model "github.com/caos/zitadel/internal/user/model"
usr_view "github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
)
@@ -111,6 +111,68 @@ func (repo *TokenVerifierRepo) ExistsOrg(ctx context.Context, orgID string) erro
return err
}
func (repo *TokenVerifierRepo) CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error {
features, err := repo.View.FeaturesByAggregateID(orgID)
if caos_errs.IsNotFound(err) {
return repo.checkDefaultFeatures(ctx, requiredFeatures...)
}
if err != nil {
return err
}
return checkFeatures(features, requiredFeatures...)
}
func checkFeatures(features *features_view_model.FeaturesView, requiredFeatures ...string) error {
for _, requiredFeature := range requiredFeatures {
if strings.HasPrefix(requiredFeature, domain.FeatureLoginPolicy) {
if err := checkLoginPolicyFeatures(features, requiredFeature); err != nil {
return err
}
}
if requiredFeature == domain.FeaturePasswordComplexityPolicy && !features.PasswordComplexityPolicy {
return MissingFeatureErr(requiredFeature)
}
if requiredFeature == domain.FeatureLabelPolicy && !features.PasswordComplexityPolicy {
return MissingFeatureErr(requiredFeature)
}
}
return nil
}
func checkLoginPolicyFeatures(features *features_view_model.FeaturesView, requiredFeature string) error {
switch requiredFeature {
case domain.FeatureLoginPolicyFactors:
if !features.LoginPolicyFactors {
return MissingFeatureErr(requiredFeature)
}
case domain.FeatureLoginPolicyIDP:
if !features.LoginPolicyIDP {
return MissingFeatureErr(requiredFeature)
}
case domain.FeatureLoginPolicyPasswordless:
if !features.LoginPolicyPasswordless {
return MissingFeatureErr(requiredFeature)
}
case domain.FeatureLoginPolicyRegistration:
if !features.LoginPolicyRegistration {
return MissingFeatureErr(requiredFeature)
}
case domain.FeatureLoginPolicyUsernameLogin:
if !features.LoginPolicyUsernameLogin {
return MissingFeatureErr(requiredFeature)
}
default:
if !features.LoginPolicyFactors && !features.LoginPolicyIDP && !features.LoginPolicyPasswordless && !features.LoginPolicyRegistration && !features.LoginPolicyUsernameLogin {
return MissingFeatureErr(requiredFeature)
}
}
return nil
}
func MissingFeatureErr(feature string) error {
return caos_errs.ThrowPermissionDeniedf(nil, "AUTH-Dvgsf", "missing feature %v", feature)
}
func (repo *TokenVerifierRepo) VerifierClientID(ctx context.Context, appName string) (_ string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -150,3 +212,36 @@ func (u *TokenVerifierRepo) getIAMByID(ctx context.Context) (*iam_model.IAM, err
}
return iam_es_model.IAMToModel(iam), nil
}
func (repo *TokenVerifierRepo) checkDefaultFeatures(ctx context.Context, requiredFeatures ...string) error {
features, viewErr := repo.View.FeaturesByAggregateID(domain.IAMID)
if viewErr != nil && !errors.IsNotFound(viewErr) {
return viewErr
}
if errors.IsNotFound(viewErr) {
features = new(features_view_model.FeaturesView)
}
events, esErr := repo.getIAMEvents(ctx, features.Sequence)
if errors.IsNotFound(viewErr) && len(events) == 0 {
return checkFeatures(features, requiredFeatures...)
}
if esErr != nil {
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
return esErr
}
featuresCopy := *features
for _, event := range events {
if err := featuresCopy.AppendEvent(event); err != nil {
return checkFeatures(features, requiredFeatures...)
}
}
return checkFeatures(&featuresCopy, requiredFeatures...)
}
func (repo *TokenVerifierRepo) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) {
query, err := iam_view.IAMByIDQuery(domain.IAMID, sequence)
if err != nil {
return nil, err
}
return repo.Eventstore.FilterEvents(ctx, query)
}

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

View File

@@ -1,9 +1,10 @@
package handler
import (
"github.com/caos/zitadel/internal/eventstore/v1"
"time"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types"
@@ -40,6 +41,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es}),
newOrg(
handler{view, bulkLimit, configs.cycleDuration("Org"), errorCount, es}),
newFeatures(
handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
}
}

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

View File

@@ -3,6 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
@@ -49,13 +50,14 @@ type Commands struct {
keyAlgorithm crypto.EncryptionAlgorithm
privateKeyLifetime time.Duration
publicKeyLifetime time.Duration
tokenVerifier *authz.TokenVerifier
}
type Config struct {
Eventstore types.SQLUser
}
func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults, authZConfig authz.Config) (repo *Commands, err error) {
func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults, authZConfig authz.Config, authZRepo *authz_repo.EsRepository) (repo *Commands, err error) {
repo = &Commands{
eventstore: eventstore,
idGenerator: id.SonyFlakeGenerator,
@@ -119,6 +121,8 @@ func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults
return nil, err
}
repo.keyAlgorithm = keyAlgorithm
repo.tokenVerifier = authz.Start(authZRepo)
return repo, nil
}

View 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()
}

View File

@@ -160,3 +160,21 @@ func writeModelToIDPProvider(wm *IdentityProviderWriteModel) *domain.IDPProvider
Type: wm.IDPProviderType,
}
}
func writeModelToFeatures(wm *FeaturesWriteModel) *domain.Features {
return &domain.Features{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
TierName: wm.TierName,
TierDescription: wm.TierDescription,
State: wm.State,
StateDescription: wm.StateDescription,
AuditLogRetention: wm.AuditLogRetention,
LoginPolicyFactors: wm.LoginPolicyFactors,
LoginPolicyIDP: wm.LoginPolicyIDP,
LoginPolicyPasswordless: wm.LoginPolicyPasswordless,
LoginPolicyRegistration: wm.LoginPolicyRegistration,
LoginPolicyUsernameLogin: wm.LoginPolicyUsernameLogin,
PasswordComplexityPolicy: wm.PasswordComplexityPolicy,
LabelPolicy: wm.LabelPolicy,
}
}

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

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

View File

@@ -221,7 +221,7 @@ func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multi
return domain.MultiFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "IAM-5m9fs", "Errors.IAM.LoginPolicy.MFA.Unspecified")
}
multiFactorModel := NewIAMMultiFactorWriteModel(multiFactor)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
event, err := c.addMultiFactorToDefaultLoginPolicy(ctx, iamAgg, multiFactorModel, multiFactor)
if err != nil {
return domain.MultiFactorTypeUnspecified, nil, err
@@ -235,7 +235,7 @@ func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multi
if err != nil {
return domain.MultiFactorTypeUnspecified, nil, err
}
return multiFactorModel.MultiFactoryWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
return multiFactorModel.MultiFactorWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
}
func (c *Commands) addMultiFactorToDefaultLoginPolicy(ctx context.Context, iamAgg *eventstore.Aggregate, multiFactorModel *IAMMultiFactorWriteModel, multiFactor domain.MultiFactorType) (eventstore.EventPusher, error) {
@@ -262,7 +262,7 @@ func (c *Commands) RemoveMultiFactorFromDefaultLoginPolicy(ctx context.Context,
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "IAM-3M9df", "Errors.IAM.LoginPolicy.MFA.NotExisting")
}
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, iam_repo.NewLoginPolicyMultiFactorRemovedEvent(ctx, iamAgg, multiFactor))
if err != nil {
return nil, err

View File

@@ -51,12 +51,12 @@ func (wm *IAMSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
}
type IAMMultiFactorWriteModel struct {
MultiFactoryWriteModel
MultiFactorWriteModel
}
func NewIAMMultiFactorWriteModel(factorType domain.MultiFactorType) *IAMMultiFactorWriteModel {
return &IAMMultiFactorWriteModel{
MultiFactoryWriteModel{
MultiFactorWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: domain.IAMID,
ResourceOwner: domain.IAMID,
@@ -82,7 +82,7 @@ func (wm *IAMMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
}
func (wm *IAMMultiFactorWriteModel) Reduce() error {
return wm.MultiFactoryWriteModel.Reduce()
return wm.MultiFactorWriteModel.Reduce()
}
func (wm *IAMMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {

View File

@@ -2,7 +2,14 @@ package command
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/repository/mock"
@@ -12,9 +19,6 @@ import (
proj_repo "github.com/caos/zitadel/internal/repository/project"
usr_repo "github.com/caos/zitadel/internal/repository/user"
"github.com/caos/zitadel/internal/repository/usergrant"
"github.com/golang/mock/gomock"
"testing"
"time"
)
type expect func(mockRepository *mock.MockRepository)
@@ -172,3 +176,48 @@ func GetMockSecretGenerator(t *testing.T) crypto.Generator {
return generator
}
func GetMockVerifier(t *testing.T, features ...string) *authz.TokenVerifier {
return authz.Start(&testVerifier{
features: features,
})
}
type testVerifier struct {
features []string
}
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, string, string, error) {
return "userID", "agentID", "de", "orgID", nil
}
func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) {
return nil, nil
}
func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID string) error {
return nil
}
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, error) {
return "clientID", nil
}
func (v *testVerifier) CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error {
for _, feature := range requiredFeatures {
hasFeature := false
for _, f := range v.features {
if f == feature {
hasFeature = true
break
}
}
if !hasFeature {
return errors.ThrowPermissionDenied(nil, "id", "missing feature")
}
}
return nil
}

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

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

View File

@@ -2,7 +2,11 @@ package command
import (
"context"
"reflect"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@@ -22,6 +26,11 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
return nil, caos_errs.ThrowAlreadyExists(nil, "Org-Dgfb2", "Errors.Org.LoginPolicy.AlreadyExists")
}
err = c.checkLoginPolicyAllowed(ctx, resourceOwner, policy)
if err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(
ctx,
@@ -43,6 +52,15 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
return writeModelToLoginPolicy(&addedPolicy.LoginPolicyWriteModel), nil
}
func (c *Commands) orgLoginPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLoginPolicyWriteModel, error) {
policyWriteModel := NewOrgLoginPolicyWriteModel(orgID)
err := c.eventstore.FilterToQueryReducer(ctx, policyWriteModel)
if err != nil {
return nil, err
}
return policyWriteModel, nil
}
func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing")
@@ -55,6 +73,12 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound")
}
err = c.checkLoginPolicyAllowed(ctx, resourceOwner, policy)
if err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel)
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType)
if !hasChanged {
@@ -72,6 +96,30 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
return writeModelToLoginPolicy(&existingPolicy.LoginPolicyWriteModel), nil
}
func (c *Commands) checkLoginPolicyAllowed(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) error {
defaultPolicy, err := c.getDefaultLoginPolicy(ctx)
if err != nil {
return err
}
requiredFeatures := make([]string, 0)
if defaultPolicy.ForceMFA != policy.ForceMFA || !reflect.DeepEqual(defaultPolicy.MultiFactors, policy.MultiFactors) || !reflect.DeepEqual(defaultPolicy.SecondFactors, policy.SecondFactors) {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyFactors)
}
if defaultPolicy.AllowExternalIDP != policy.AllowExternalIDP || !reflect.DeepEqual(defaultPolicy.IDPProviders, policy.IDPProviders) {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyIDP)
}
if defaultPolicy.AllowRegister != policy.AllowRegister {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyRegistration)
}
if defaultPolicy.PasswordlessType != policy.PasswordlessType {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyPasswordless)
}
if defaultPolicy.AllowUsernamePassword != policy.AllowUsernamePassword {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyUsernameLogin)
}
return authz.CheckOrgFeatures(ctx, c.tokenVerifier, resourceOwner, requiredFeatures...)
}
func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-55Mg9", "Errors.ResourceOwnerMissing")
@@ -267,7 +315,7 @@ func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor
if err != nil {
return domain.MultiFactorTypeUnspecified, nil, err
}
return multiFactorModel.MultiFactoryWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
return multiFactorModel.MultiFactorWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
}
func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType, orgID string) (*domain.ObjectDetails, error) {
@@ -285,7 +333,7 @@ func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFa
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LoginPolicy.MFA.NotExisting")
}
orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicyMultiFactorRemovedEvent(ctx, orgAgg, multiFactor))
if err != nil {

View File

@@ -51,12 +51,12 @@ func (wm *OrgSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
}
type OrgMultiFactorWriteModel struct {
MultiFactoryWriteModel
MultiFactorWriteModel
}
func NewOrgMultiFactorWriteModel(orgID string, factorType domain.MultiFactorType) *OrgMultiFactorWriteModel {
return &OrgMultiFactorWriteModel{
MultiFactoryWriteModel{
MultiFactorWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
@@ -82,7 +82,7 @@ func (wm *OrgMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
}
func (wm *OrgMultiFactorWriteModel) Reduce() error {
return wm.MultiFactoryWriteModel.Reduce()
return wm.MultiFactorWriteModel.Reduce()
}
func (wm *OrgMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {

View File

@@ -6,11 +6,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/repository/iam"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/policy"
"github.com/caos/zitadel/internal/repository/user"
@@ -18,7 +20,8 @@ import (
func TestCommandSide_AddLoginPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore *eventstore.Eventstore
tokenVerifier *authz.TokenVerifier
}
type args struct {
ctx context.Context
@@ -88,12 +91,60 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
err: caos_errs.IsErrorAlreadyExists,
},
},
{
name: "loginpolicy not allowed, permission denied error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
),
tokenVerifier: GetMockVerifier(t),
},
args: args{
ctx: context.Background(),
orgID: "org1",
policy: &domain.LoginPolicy{
AllowRegister: true,
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
},
},
res: res{
err: caos_errs.IsPermissionDenied,
},
},
{
name: "add policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -109,6 +160,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
},
),
),
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
},
args: args{
ctx: context.Background(),
@@ -139,7 +191,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
if tt.res.err == nil {
@@ -157,7 +210,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore *eventstore.Eventstore
tokenVerifier *authz.TokenVerifier
}
type args struct {
ctx context.Context
@@ -218,6 +272,53 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
err: caos_errs.IsNotFound,
},
},
{
name: "not allowed, permission denied error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
),
tokenVerifier: GetMockVerifier(t),
},
args: args{
ctx: context.Background(),
orgID: "org1",
policy: &domain.LoginPolicy{
AllowRegister: true,
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
},
},
res: res{
err: caos_errs.IsPermissionDenied,
},
},
{
name: "no changes, precondition error",
fields: fields{
@@ -235,7 +336,20 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
),
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
},
args: args{
ctx: context.Background(),
@@ -269,6 +383,18 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -277,6 +403,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
},
),
),
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
},
args: args{
ctx: context.Background(),
@@ -307,7 +434,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
if tt.res.err == nil {

View File

@@ -26,13 +26,13 @@ func (wm *SecondFactorWriteModel) Reduce() error {
return wm.WriteModel.Reduce()
}
type MultiFactoryWriteModel struct {
type MultiFactorWriteModel struct {
eventstore.WriteModel
MFAType domain.MultiFactorType
State domain.FactorState
}
func (wm *MultiFactoryWriteModel) Reduce() error {
func (wm *MultiFactorWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *policy.MultiFactorAddedEvent:

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

View File

@@ -23,7 +23,7 @@ func (s *Step9) execute(ctx context.Context, commandSide *Commands) error {
func (c *Commands) SetupStep9(ctx context.Context, step *Step9) error {
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
multiFactorModel := NewIAMMultiFactorWriteModel(domain.MultiFactorTypeU2FWithPIN)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
if !step.Passwordless {
return []eventstore.EventPusher{}, nil
}

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

View File

@@ -1,8 +1,9 @@
package domain
import (
"github.com/caos/zitadel/internal/api/authz"
"strings"
"github.com/caos/zitadel/internal/api/authz"
)
const (

View File

@@ -14,6 +14,7 @@ const (
Step9
Step10
Step11
Step12
//StepCount marks the the length of possible steps (StepCount-1 == last possible step)
StepCount
)

View File

@@ -181,6 +181,8 @@ func getField(field es_models.Field) string {
return "editor_user"
case es_models.Field_EventType:
return "event_type"
case es_models.Field_CreationDate:
return "creation_date"
}
return ""
}

View File

@@ -10,4 +10,5 @@ const (
Field_EditorService
Field_EditorUser
Field_EventType
Field_CreationDate
)

View File

@@ -1,6 +1,8 @@
package models
import (
"time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/errors"
)
@@ -15,6 +17,7 @@ type SearchQueryFactory struct {
sequenceTo uint64
eventTypes []EventType
resourceOwner string
creationDate time.Time
}
type searchQuery struct {
@@ -63,7 +66,8 @@ func FactoryFromSearchQuery(query *SearchQuery) *SearchQueryFactory {
factory = factory.EventTypes(filter.value.([]EventType)...)
case Field_EditorService, Field_EditorUser:
logging.Log("MODEL-Mr0VN").WithField("value", filter.value).Panic("field not converted to factory")
case Field_CreationDate:
factory = factory.CreationDateNewer(filter.value.(time.Time))
}
}
@@ -116,6 +120,11 @@ func (factory *SearchQueryFactory) ResourceOwner(resourceOwner string) *SearchQu
return factory
}
func (factory *SearchQueryFactory) CreationDateNewer(time time.Time) *SearchQueryFactory {
factory.creationDate = time
return factory
}
func (factory *SearchQueryFactory) OrderDesc() *SearchQueryFactory {
factory.desc = true
return factory
@@ -142,6 +151,7 @@ func (factory *SearchQueryFactory) Build() (*searchQuery, error) {
factory.sequenceToFilter,
factory.eventTypeFilter,
factory.resourceOwnerFilter,
factory.creationDateNewerFilter,
} {
if filter := f(); filter != nil {
filters = append(filters, filter)
@@ -211,3 +221,10 @@ func (factory *SearchQueryFactory) resourceOwnerFilter() *Filter {
}
return NewFilter(Field_ResourceOwner, factory.resourceOwner, Operation_Equals)
}
func (factory *SearchQueryFactory) creationDateNewerFilter() *Filter {
if factory.creationDate.IsZero() {
return nil
}
return NewFilter(Field_CreationDate, factory.creationDate, Operation_Greater)
}

View File

@@ -1,6 +1,10 @@
package models
import "github.com/caos/zitadel/internal/errors"
import (
"time"
"github.com/caos/zitadel/internal/errors"
)
//SearchQuery is deprecated. Use SearchQueryFactory
type SearchQuery struct {
@@ -68,6 +72,10 @@ func (q *SearchQuery) ResourceOwnerFilter(resourceOwner string) *SearchQuery {
return q.setFilter(NewFilter(Field_ResourceOwner, resourceOwner, Operation_Equals))
}
func (q *SearchQuery) CreationDateNewerFilter(time time.Time) *SearchQuery {
return q.setFilter(NewFilter(Field_CreationDate, time, Operation_Greater))
}
func (q *SearchQuery) setFilter(filter *Filter) *SearchQuery {
for i, f := range q.Filters {
if f.field == filter.field && f.field != Field_LatestSequence {

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

View 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...)
}

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

View 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 ""
}
}

View File

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

View File

@@ -12,6 +12,7 @@ import (
"github.com/caos/zitadel/internal/user/repository/view"
"github.com/golang/protobuf/ptypes"
"strings"
"time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/authz"
@@ -93,8 +94,8 @@ func (repo *OrgRepository) SearchMyOrgDomains(ctx context.Context, request *org_
return result, nil
}
func (repo *OrgRepository) OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*org_model.OrgChanges, error) {
changes, err := repo.getOrgChanges(ctx, id, lastSequence, limit, sortAscending)
func (repo *OrgRepository) OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error) {
changes, err := repo.getOrgChanges(ctx, id, lastSequence, limit, sortAscending, auditLogRetention)
if err != nil {
return nil, err
}
@@ -537,8 +538,8 @@ func (repo *OrgRepository) GetMailTexts(ctx context.Context) (*iam_model.MailTex
return iam_es_model.MailTextsViewToModel(texts, defaultIn), err
}
func (repo *OrgRepository) getOrgChanges(ctx context.Context, orgID string, lastSequence uint64, limit uint64, sortAscending bool) (*org_model.OrgChanges, error) {
query := org_view.ChangesQuery(orgID, lastSequence, limit, sortAscending)
func (repo *OrgRepository) getOrgChanges(ctx context.Context, orgID string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error) {
query := org_view.ChangesQuery(orgID, lastSequence, limit, sortAscending, auditLogRetention)
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
if err != nil {

View File

@@ -10,6 +10,7 @@ import (
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
"strings"
"time"
"github.com/caos/logging"
"github.com/golang/protobuf/ptypes"
@@ -180,8 +181,8 @@ func (repo *ProjectRepo) SearchProjectRoles(ctx context.Context, projectID strin
return result, nil
}
func (repo *ProjectRepo) ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ProjectChanges, error) {
changes, err := repo.getProjectChanges(ctx, id, lastSequence, limit, sortAscending)
func (repo *ProjectRepo) ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ProjectChanges, error) {
changes, err := repo.getProjectChanges(ctx, id, lastSequence, limit, sortAscending, retention)
if err != nil {
return nil, err
}
@@ -254,8 +255,8 @@ func (repo *ProjectRepo) SearchApplications(ctx context.Context, request *proj_m
return result, nil
}
func (repo *ProjectRepo) ApplicationChanges(ctx context.Context, id string, appId string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ApplicationChanges, error) {
changes, err := repo.getApplicationChanges(ctx, id, appId, lastSequence, limit, sortAscending)
func (repo *ProjectRepo) ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ApplicationChanges, error) {
changes, err := repo.getApplicationChanges(ctx, projectID, appID, lastSequence, limit, sortAscending, retention)
if err != nil {
return nil, err
}
@@ -505,8 +506,8 @@ func (r *ProjectRepo) getUserEvents(ctx context.Context, userID string, sequence
return r.Eventstore.FilterEvents(ctx, query)
}
func (repo *ProjectRepo) getProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ProjectChanges, error) {
query := proj_view.ChangesQuery(id, lastSequence, limit, sortAscending)
func (repo *ProjectRepo) getProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ProjectChanges, error) {
query := proj_view.ChangesQuery(id, lastSequence, limit, sortAscending, retention)
events, err := repo.Eventstore.FilterEvents(context.Background(), query)
if err != nil {
@@ -561,8 +562,8 @@ func (repo *ProjectRepo) getProjectEvents(ctx context.Context, id string, sequen
return repo.Eventstore.FilterEvents(ctx, query)
}
func (repo *ProjectRepo) getApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool) (*proj_model.ApplicationChanges, error) {
query := proj_view.ChangesQuery(projectID, lastSequence, limit, sortAscending)
func (repo *ProjectRepo) getApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ApplicationChanges, error) {
query := proj_view.ChangesQuery(projectID, lastSequence, limit, sortAscending, retention)
events, err := repo.Eventstore.FilterEvents(ctx, query)
if err != nil {

View File

@@ -2,6 +2,8 @@ package eventstore
import (
"context"
"time"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
@@ -82,8 +84,8 @@ func (repo *UserRepo) UserIDsByDomain(ctx context.Context, domain string) ([]str
return repo.View.UserIDsByDomain(domain)
}
func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*usr_model.UserChanges, error) {
changes, err := repo.getUserChanges(ctx, id, lastSequence, limit, sortAscending)
func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*usr_model.UserChanges, error) {
changes, err := repo.getUserChanges(ctx, id, lastSequence, limit, sortAscending, retention)
if err != nil {
return nil, err
}
@@ -280,8 +282,8 @@ func (repo *UserRepo) SearchUserMemberships(ctx context.Context, request *usr_mo
return result, nil
}
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool) (*usr_model.UserChanges, error) {
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending)
func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*usr_model.UserChanges, error) {
query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention)
events, err := r.Eventstore.FilterEvents(ctx, query)
if err != nil {

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

View File

@@ -1,9 +1,10 @@
package handler
import (
"github.com/caos/zitadel/internal/eventstore/v1"
"time"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/eventstore/v1/query"
@@ -76,6 +77,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}),
newMailText(
handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}),
newFeatures(
handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
}
}

View File

@@ -27,6 +27,7 @@ type EsRepository struct {
eventstore.UserRepo
eventstore.UserGrantRepo
eventstore.IAMRepository
eventstore.FeaturesRepo
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},
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults},
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, view},
IAMRepository: eventstore.IAMRepository{
IAMV2Query: queries,
},
view: view,
IAMRepository: eventstore.IAMRepository{IAMV2Query: queries},
FeaturesRepo: eventstore.FeaturesRepo{es, view, conf.SearchLimit, systemDefaults},
view: view,
}, nil
}

View 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 = "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)
}

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

View File

@@ -2,6 +2,7 @@ package repository
import (
"context"
"time"
iam_model "github.com/caos/zitadel/internal/iam/model"
@@ -11,7 +12,7 @@ import (
type OrgRepository interface {
OrgByID(ctx context.Context, id string) (*org_model.OrgView, error)
OrgByDomainGlobal(ctx context.Context, domain string) (*org_model.OrgView, error)
OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*org_model.OrgChanges, error)
OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error)
SearchMyOrgDomains(ctx context.Context, request *org_model.OrgDomainSearchRequest) (*org_model.OrgDomainSearchResponse, error)

View File

@@ -2,6 +2,8 @@ package repository
import (
"context"
"time"
iam_model "github.com/caos/zitadel/internal/iam/model"
key_model "github.com/caos/zitadel/internal/key/model"
@@ -22,11 +24,11 @@ type ProjectRepository interface {
GetProjectMemberRoles(ctx context.Context) ([]string, error)
SearchProjectRoles(ctx context.Context, projectId string, request *model.ProjectRoleSearchRequest) (*model.ProjectRoleSearchResponse, error)
ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*model.ProjectChanges, error)
ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.ProjectChanges, error)
ApplicationByID(ctx context.Context, projectID, appID string) (*model.ApplicationView, error)
SearchApplications(ctx context.Context, request *model.ApplicationSearchRequest) (*model.ApplicationSearchResponse, error)
ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool) (*model.ApplicationChanges, error)
ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.ApplicationChanges, error)
SearchClientKeys(ctx context.Context, request *key_model.AuthNKeySearchRequest) (*key_model.AuthNKeySearchResponse, error)
GetClientKey(ctx context.Context, projectID, applicationID, keyID string) (*key_model.AuthNKeyView, error)

View File

@@ -7,4 +7,5 @@ type Repository interface {
UserRepository
UserGrantRepository
IamRepository
FeaturesRepository
}

View File

@@ -2,6 +2,7 @@ package repository
import (
"context"
"time"
key_model "github.com/caos/zitadel/internal/key/model"
"github.com/caos/zitadel/internal/user/model"
@@ -15,7 +16,7 @@ type UserRepository interface {
GetUserByLoginNameGlobal(ctx context.Context, email string) (*model.UserView, error)
IsUserUnique(ctx context.Context, userName, email string) (bool, error)
UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error)
UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error)
ProfileByID(ctx context.Context, userID string) (*model.Profile, error)

View File

@@ -1,6 +1,8 @@
package view
import (
"time"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
@@ -36,13 +38,16 @@ func OrgNameUniqueQuery(name string) *es_models.SearchQuery {
SetLimit(1)
}
func ChangesQuery(orgID string, latestSequence, limit uint64, sortAscending bool) *es_models.SearchQuery {
func ChangesQuery(orgID string, latestSequence, limit uint64, sortAscending bool, auditLogRetention time.Duration) *es_models.SearchQuery {
query := es_models.NewSearchQuery().
AggregateTypeFilter(model.OrgAggregate)
if !sortAscending {
query.OrderDesc()
}
if auditLogRetention > 0 {
query.CreationDateNewerFilter(time.Now().Add(-auditLogRetention))
}
query.LatestSequenceFilter(latestSequence).
AggregateIDFilter(orgID).

View File

@@ -1,6 +1,8 @@
package view
import (
"time"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
@@ -20,12 +22,15 @@ func ProjectQuery(latestSequence uint64) *es_models.SearchQuery {
LatestSequenceFilter(latestSequence)
}
func ChangesQuery(projectID string, latestSequence, limit uint64, sortAscending bool) *es_models.SearchQuery {
func ChangesQuery(projectID string, latestSequence, limit uint64, sortAscending bool, retention time.Duration) *es_models.SearchQuery {
query := es_models.NewSearchQuery().
AggregateTypeFilter(model.ProjectAggregate)
if !sortAscending {
query.OrderDesc()
}
if retention > 0 {
query.CreationDateNewerFilter(time.Now().Add(-retention))
}
query.LatestSequenceFilter(latestSequence).
AggregateIDFilter(projectID).

View File

@@ -14,4 +14,5 @@ extend google.protobuf.MethodOptions {
message AuthOption {
string permission = 1;
string check_field_name = 2;
string feature = 3;
}

View File

@@ -21,10 +21,11 @@ const {{$s.Name}}_MethodPrefix = "{{$.File.Package}}.{{$s.Name}}"
var {{$s.Name}}_AuthMethods = authz.MethodMapping {
{{ range $m := $s.Method}}
{{ $mAuthOpt := option $m.Options "zitadel.v1.auth_option" }}
{{ if and $mAuthOpt $mAuthOpt.Permission }}
{{ if and $mAuthOpt (or $mAuthOpt.Permission $mAuthOpt.Feature) }}
"/{{$.File.Package}}.{{$s.Name}}/{{.Name}}": authz.Option{
Permission: "{{$mAuthOpt.Permission}}",
CheckParam: "{{$mAuthOpt.CheckFieldName}}",
Feature: "{{$mAuthOpt.Feature}}",
},
{{end}}
{{ end}}

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

View File

@@ -42,5 +42,6 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(MailTemplateAddedEventType, MailTemplateAddedEventMapper).
RegisterFilterEventMapper(MailTemplateChangedEventType, MailTemplateChangedEventMapper).
RegisterFilterEventMapper(MailTextAddedEventType, MailTextAddedEventMapper).
RegisterFilterEventMapper(MailTextChangedEventType, MailTextChangedEventMapper)
RegisterFilterEventMapper(MailTextChangedEventType, MailTextChangedEventMapper).
RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper)
}

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

View File

@@ -55,5 +55,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(IDPConfigDeactivatedEventType, IDPConfigDeactivatedEventMapper).
RegisterFilterEventMapper(IDPConfigReactivatedEventType, IDPConfigReactivatedEventMapper).
RegisterFilterEventMapper(IDPOIDCConfigAddedEventType, IDPOIDCConfigAddedEventMapper).
RegisterFilterEventMapper(IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper)
RegisterFilterEventMapper(IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper).
RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper).
RegisterFilterEventMapper(FeaturesRemovedEventType, FeaturesRemovedEventMapper)
}

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

View File

@@ -17,6 +17,7 @@ type IAMSetUp struct {
Step9 *command.Step9
Step10 *command.Step10
Step11 *command.Step11
Step12 *command.Step12
}
func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) {
@@ -34,6 +35,7 @@ func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) {
setup.Step9,
setup.Step10,
setup.Step11,
setup.Step12,
} {
if step.Step() <= currentDone {
continue

View File

@@ -1,6 +1,8 @@
package view
import (
"time"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@@ -20,12 +22,15 @@ func UserQuery(latestSequence uint64) *es_models.SearchQuery {
LatestSequenceFilter(latestSequence)
}
func ChangesQuery(userID string, latestSequence, limit uint64, sortAscending bool) *es_models.SearchQuery {
func ChangesQuery(userID string, latestSequence, limit uint64, sortAscending bool, retention time.Duration) *es_models.SearchQuery {
query := es_models.NewSearchQuery().
AggregateTypeFilter(model.UserAggregate)
if !sortAscending {
query.OrderDesc()
}
if retention > 0 {
query.CreationDateNewerFilter(time.Now().Add(-retention))
}
query.LatestSequenceFilter(latestSequence).
AggregateIDFilter(userID).

View 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)
);

View File

@@ -7,9 +7,11 @@ import "zitadel/options.proto";
import "zitadel/org.proto";
import "zitadel/policy.proto";
import "zitadel/member.proto";
import "zitadel/features.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
@@ -44,7 +46,7 @@ service AdminService {
get: "/healthz"
};
}
rpc IsOrgUnique(IsOrgUniqueRequest) returns (IsOrgUniqueResponse) {
option (google.api.http) = {
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) {
option (google.api.http) = {
get: "/policies/orgiam"
@@ -259,18 +313,18 @@ service AdminService {
option (google.api.http) = {
get: "/policies/login"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read"
};
}
rpc UpdateLoginPolicy(UpdateLoginPolicyRequest) returns (UpdateLoginPolicyResponse) {
option (google.api.http) = {
put: "/policies/login"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write"
};
@@ -395,18 +449,18 @@ service AdminService {
option (google.api.http) = {
get: "/policies/password/age"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read"
};
}
rpc UpdatePasswordAgePolicy(UpdatePasswordAgePolicyRequest) returns (UpdatePasswordAgePolicyResponse) {
option (google.api.http) = {
put: "/policies/password/age"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write"
};
@@ -416,18 +470,18 @@ service AdminService {
option (google.api.http) = {
get: "/policies/password/lockout"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read"
};
}
rpc UpdatePasswordLockoutPolicy(UpdatePasswordLockoutPolicyRequest) returns (UpdatePasswordLockoutPolicyResponse) {
option (google.api.http) = {
put: "/policies/password/lockout"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write"
};
@@ -583,9 +637,9 @@ message SetUpOrgRequest {
string phone = 1 [(validate.rules).string = {min_len: 1, max_len: 50, prefix: "+"}];
bool is_phone_verified = 2;
}
string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
Profile profile = 2 [(validate.rules).message.required = true];
Email email = 3 [(validate.rules).message.required = true];
Phone phone = 4;
@@ -697,6 +751,66 @@ message UpdateIDPOIDCConfigResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultFeaturesRequest {}
message GetDefaultFeaturesResponse {
zitadel.features.v1.Features features = 1;
}
message SetDefaultFeaturesRequest {
string tier_name = 1 [(validate.rules).string = {max_len: 200}];
string description = 2 [(validate.rules).string = {max_len: 200}];
google.protobuf.Duration audit_log_retention = 5 [(validate.rules).duration = {gte: {seconds: 0}}];
bool login_policy_username_login = 6;
bool login_policy_registration = 7;
bool login_policy_idp = 8;
bool login_policy_factors = 9;
bool login_policy_passwordless = 10;
bool password_complexity_policy = 11;
bool label_policy = 12;
}
message SetDefaultFeaturesResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetOrgFeaturesRequest {
string org_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetOrgFeaturesResponse {
zitadel.features.v1.Features features = 1;
}
message SetOrgFeaturesRequest {
string org_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string tier_name = 2 [(validate.rules).string = {max_len: 200}];
string description = 3 [(validate.rules).string = {max_len: 200}];
zitadel.features.v1.FeaturesState state = 4;
string state_description = 5 [(validate.rules).string = {max_len: 200}];
google.protobuf.Duration audit_log_retention = 6 [(validate.rules).duration = {gte: {seconds: 0}}];
bool login_policy_username_login = 7;
bool login_policy_registration = 8;
bool login_policy_idp = 9;
bool login_policy_factors = 10;
bool login_policy_passwordless = 11;
bool password_complexity_policy = 12;
bool label_policy = 13;
}
message SetOrgFeaturesResponse {
zitadel.v1.ObjectDetails details = 1;
}
message ResetOrgFeaturesRequest {
string org_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message ResetOrgFeaturesResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetOrgIAMPolicyRequest {}
message GetOrgIAMPolicyResponse {

View File

@@ -375,6 +375,16 @@ service AuthService {
};
}
rpc ListMyZitadelFeatures(ListMyZitadelFeaturesRequest) returns (ListMyZitadelFeaturesResponse) {
option (google.api.http) = {
post: "/features/zitadel/me/_search"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
rpc ListMyZitadelPermissions(ListMyZitadelPermissionsRequest) returns (ListMyZitadelPermissionsResponse) {
option (google.api.http) = {
post: "/permissions/zitadel/me/_search"
@@ -658,6 +668,12 @@ message ListMyProjectOrgsResponse {
repeated zitadel.org.v1.Org result = 2;
}
message ListMyZitadelFeaturesRequest {}
message ListMyZitadelFeaturesResponse {
repeated string result = 1;
}
message ListMyZitadelPermissionsRequest {}
message ListMyZitadelPermissionsResponse {

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

View File

@@ -12,6 +12,7 @@ import "zitadel/policy.proto";
import "zitadel/message.proto";
import "zitadel/change.proto";
import "zitadel/auth_n_key.proto";
import "zitadel/features.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
@@ -44,7 +45,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
consumes: "application/grpc";
produces: "application/grpc";
@@ -242,7 +243,7 @@ service ManagementService {
option (google.api.http) = {
get: "/users/{user_id}/username"
};
option (zitadel.v1.auth_option) = {
permission: "user.write"
};
@@ -292,7 +293,7 @@ service ManagementService {
rpc ResendHumanInitialization(ResendHumanInitializationRequest) returns (ResendHumanInitializationResponse) {
option (google.api.http) = {
post: "/users/{user_id}/_resend_initialization"
post: "/users/{user_id}/_resend_initialization"
body: "*"
};
@@ -310,7 +311,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "user.write"
};
}
}
rpc GetHumanPhone(GetHumanPhoneRequest) returns (GetHumanPhoneResponse) {
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) {
option (google.api.http) = {
get: "/policies/orgiam"
@@ -1378,6 +1389,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy"
};
}
@@ -1389,6 +1401,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy"
};
}
@@ -1421,6 +1434,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy.idp"
};
}
@@ -1431,6 +1445,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy.idp"
};
}
@@ -1452,6 +1467,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy.factors"
};
}
@@ -1462,6 +1478,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy.factors"
};
}
@@ -1483,6 +1500,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy.factors"
};
}
@@ -1493,6 +1511,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "login_policy.factors"
};
}
@@ -1524,6 +1543,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "password_complexity_policy"
};
}
@@ -1535,6 +1555,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "password_complexity_policy"
};
}
@@ -1680,6 +1701,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "label_policy"
};
}
@@ -1691,6 +1713,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "label_policy"
};
}
@@ -1733,6 +1756,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
feature: "login_policy.idp"
};
}
@@ -1744,6 +1768,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
feature: "login_policy.idp"
};
}
@@ -1755,6 +1780,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
feature: "login_policy.idp"
};
}
@@ -1765,6 +1791,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
feature: "login_policy.idp"
};
}
@@ -1776,6 +1803,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
feature: "login_policy.idp"
};
}
@@ -1787,6 +1815,7 @@ service ManagementService {
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
feature: "login_policy.idp"
};
}
}
@@ -2956,6 +2985,12 @@ message BulkRemoveUserGrantRequest {
message BulkRemoveUserGrantResponse {}
message GetFeaturesRequest {}
message GetFeaturesResponse {
zitadel.features.v1.Features features = 1;
}
message GetOrgIAMPolicyRequest {}
message GetOrgIAMPolicyResponse {

View File

@@ -14,4 +14,5 @@ extend google.protobuf.MethodOptions {
message AuthOption {
string permission = 1;
string check_field_name = 2;
}
string feature = 3;
}