From beb1c1604af3a14db57ae3175355ce8ba8f2c794 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Mon, 5 Jul 2021 10:36:51 +0200 Subject: [PATCH] feat: Privacy policy (#1957) * feat: command side privacy policy * feat: add privacy policy to api * feat: add privacy policy query side * fix: add privacy policy to mgmt api * fix: add privacy policy to auth and base data * feat: use privacyPolicy in login gui * feat: use privacyPolicy in login gui * feat: test org fatures * feat: typos * feat: tos in register --- cmd/zitadel/setup.yaml | 6 +- docs/docs/apis/proto/admin.md | 63 +++ docs/docs/apis/proto/management.md | 152 ++++++ docs/docs/apis/proto/policy.md | 14 + .../eventsourcing/eventstore/iam.go | 27 + .../eventsourcing/handler/handler.go | 2 + .../eventsourcing/handler/privacy_policy.go | 106 ++++ .../eventsourcing/view/privacy_policy.go | 53 ++ internal/admin/repository/iam.go | 2 + internal/api/grpc/admin/features.go | 2 + internal/api/grpc/admin/privacy_policy.go | 31 ++ .../grpc/admin/privacy_policy_converter.go | 13 + internal/api/grpc/features/features.go | 1 + .../api/grpc/management/policy_privacy.go | 64 +++ .../management/policy_privacy_converter.go | 20 + internal/api/grpc/policy/privacy_policy.go | 21 + .../eventsourcing/eventstore/auth_request.go | 20 + .../eventsourcing/eventstore/org.go | 8 + .../eventsourcing/handler/handler.go | 1 + .../eventsourcing/handler/privacy_policy.go | 106 ++++ .../eventsourcing/view/privacy_policy.go | 53 ++ internal/auth/repository/org.go | 1 + .../eventstore/token_verifier.go | 6 + internal/command/features_model.go | 1 + internal/command/iam_converter.go | 8 + internal/command/iam_features.go | 1 + internal/command/iam_features_model.go | 7 +- internal/command/iam_policy_privacy.go | 92 ++++ internal/command/iam_policy_privacy_model.go | 73 +++ internal/command/iam_policy_privacy_test.go | 292 +++++++++++ internal/command/org_converter.go | 8 + internal/command/org_features.go | 10 + internal/command/org_features_model.go | 6 +- internal/command/org_features_test.go | 64 +++ internal/command/org_policy_privacy.go | 135 +++++ internal/command/org_policy_privacy_model.go | 74 +++ internal/command/org_policy_privacy_test.go | 479 ++++++++++++++++++ internal/command/policy_privacy_model.go | 36 ++ internal/command/setup_step17.go | 35 ++ internal/domain/auth_request.go | 1 + internal/domain/features.go | 2 + internal/domain/policy_privacy.go | 15 + internal/domain/step.go | 1 + internal/features/model/features_view.go | 4 + .../repository/view/model/features.go | 2 + internal/iam/model/privacy_policy_view.go | 48 ++ .../repository/eventsourcing/model/types.go | 3 + .../repository/view/model/privacy_policy.go | 98 ++++ .../view/model/privacy_policy_query.go | 59 +++ .../repository/view/privacy_policy_view.go | 32 ++ .../eventsourcing/eventstore/org.go | 17 + .../eventsourcing/handler/handler.go | 2 + .../eventsourcing/handler/privacy_policy.go | 106 ++++ .../eventsourcing/view/privacy_policy.go | 53 ++ internal/management/repository/org.go | 3 + .../repository/eventsourcing/model/types.go | 4 + internal/repository/features/features.go | 8 + internal/repository/iam/eventstore.go | 2 + internal/repository/iam/policy_privacy.go | 75 +++ internal/repository/org/eventstore.go | 3 + internal/repository/org/policy_privacy.go | 103 ++++ internal/repository/policy/policy_privacy.go | 136 +++++ internal/setup/config.go | 2 + .../login/handler/privacy_policy_handler.go | 15 + internal/ui/login/handler/renderer.go | 16 +- internal/ui/login/static/i18n/de.yaml | 6 - internal/ui/login/static/i18n/en.yaml | 6 - .../ui/login/static/templates/footer.html | 9 +- .../ui/login/static/templates/register.html | 24 +- .../login/static/templates/register_org.html | 23 +- .../cockroach/V1.50__privacy_policy.sql | 46 ++ proto/zitadel/admin.proto | 77 +++ proto/zitadel/features.proto | 1 + proto/zitadel/management.proto | 103 ++++ proto/zitadel/policy.proto | 7 + 75 files changed, 3171 insertions(+), 34 deletions(-) create mode 100644 internal/admin/repository/eventsourcing/handler/privacy_policy.go create mode 100644 internal/admin/repository/eventsourcing/view/privacy_policy.go create mode 100644 internal/api/grpc/admin/privacy_policy.go create mode 100644 internal/api/grpc/admin/privacy_policy_converter.go create mode 100644 internal/api/grpc/management/policy_privacy.go create mode 100644 internal/api/grpc/management/policy_privacy_converter.go create mode 100644 internal/api/grpc/policy/privacy_policy.go create mode 100644 internal/auth/repository/eventsourcing/handler/privacy_policy.go create mode 100644 internal/auth/repository/eventsourcing/view/privacy_policy.go create mode 100644 internal/command/iam_policy_privacy.go create mode 100644 internal/command/iam_policy_privacy_model.go create mode 100644 internal/command/iam_policy_privacy_test.go create mode 100644 internal/command/org_policy_privacy.go create mode 100644 internal/command/org_policy_privacy_model.go create mode 100644 internal/command/org_policy_privacy_test.go create mode 100644 internal/command/policy_privacy_model.go create mode 100644 internal/command/setup_step17.go create mode 100644 internal/domain/policy_privacy.go create mode 100644 internal/iam/model/privacy_policy_view.go create mode 100644 internal/iam/repository/view/model/privacy_policy.go create mode 100644 internal/iam/repository/view/model/privacy_policy_query.go create mode 100644 internal/iam/repository/view/privacy_policy_view.go create mode 100644 internal/management/repository/eventsourcing/handler/privacy_policy.go create mode 100644 internal/management/repository/eventsourcing/view/privacy_policy.go create mode 100644 internal/repository/iam/policy_privacy.go create mode 100644 internal/repository/org/policy_privacy.go create mode 100644 internal/repository/policy/policy_privacy.go create mode 100644 internal/ui/login/handler/privacy_policy_handler.go create mode 100644 migrations/cockroach/V1.50__privacy_policy.sql diff --git a/cmd/zitadel/setup.yaml b/cmd/zitadel/setup.yaml index 8eff6e25f4..64d8f055a4 100644 --- a/cmd/zitadel/setup.yaml +++ b/cmd/zitadel/setup.yaml @@ -188,4 +188,8 @@ SetUp: Subject: Domain has been claimed Greeting: Hello {{.FirstName}} {{.LastName}}, Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.UserName}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login. - ButtonText: Login \ No newline at end of file + ButtonText: Login + Step17: + PrivacyPolicy: + TOSLink: https://docs.zitadel.ch/docs/legal/terms-of-service + PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy \ No newline at end of file diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 613a43cd50..d113099d97 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -528,6 +528,27 @@ it impacts all organisations without a customised policy +### GetPrivacyPolicy + +> **rpc** GetPrivacyPolicy([GetPrivacyPolicyRequest](#getprivacypolicyrequest)) +[GetPrivacyPolicyResponse](#getprivacypolicyresponse) + +Returns the privacy policy defined by the administrators of ZITADEL + + + + +### UpdatePrivacyPolicy + +> **rpc** UpdatePrivacyPolicy([UpdatePrivacyPolicyRequest](#updateprivacypolicyrequest)) +[UpdatePrivacyPolicyResponse](#updateprivacypolicyresponse) + +Updates the default privacy policy of ZITADEL +it impacts all organisations without a customised policy + + + + ### GetDefaultInitMessageText > **rpc** GetDefaultInitMessageText([GetDefaultInitMessageTextRequest](#getdefaultinitmessagetextrequest)) @@ -1304,6 +1325,23 @@ This is an empty request +### GetPrivacyPolicyRequest +This is an empty request + + + + +### GetPrivacyPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| policy | zitadel.policy.v1.PrivacyPolicy | - | | + + + + ### HealthzRequest This is an empty request @@ -1868,6 +1906,7 @@ This is an empty request | label_policy_private_label | bool | - | | | label_policy_watermark | bool | - | | | custom_text | bool | - | | +| privacy_policy | bool | - | | @@ -2023,6 +2062,7 @@ This is an empty request | label_policy_private_label | bool | - | | | label_policy_watermark | bool | - | | | custom_text | bool | - | | +| privacy_policy | bool | - | | @@ -2380,6 +2420,29 @@ This is an empty request +### UpdatePrivacyPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| tos_link | string | - | | +| privacy_link | string | - | | + + + + +### UpdatePrivacyPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### View diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index f75bc29ce3..e9b1a9c09e 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -1586,6 +1586,61 @@ The password lockout policy is not used at the moment +### GetPrivacyPolicy + +> **rpc** GetPrivacyPolicy([GetPrivacyPolicyRequest](#getprivacypolicyrequest)) +[GetPrivacyPolicyResponse](#getprivacypolicyresponse) + +Returns the privacy policy of the organisation +With this policy privacy relevant things can be configured (e.g. tos link) + + + + +### GetDefaultPrivacyPolicy + +> **rpc** GetDefaultPrivacyPolicy([GetDefaultPrivacyPolicyRequest](#getdefaultprivacypolicyrequest)) +[GetDefaultPrivacyPolicyResponse](#getdefaultprivacypolicyresponse) + +Returns the default privacy policy of the IAM +With this policy the privacy relevant things can be configured (e.g tos link) + + + + +### AddCustomPrivacyPolicy + +> **rpc** AddCustomPrivacyPolicy([AddCustomPrivacyPolicyRequest](#addcustomprivacypolicyrequest)) +[AddCustomPrivacyPolicyResponse](#addcustomprivacypolicyresponse) + +Add a custom privacy policy for the organisation +With this policy privacy relevant things can be configured (e.g. tos link) + + + + +### UpdateCustomPrivacyPolicy + +> **rpc** UpdateCustomPrivacyPolicy([UpdateCustomPrivacyPolicyRequest](#updatecustomprivacypolicyrequest)) +[UpdateCustomPrivacyPolicyResponse](#updatecustomprivacypolicyresponse) + +Update the privacy complexity policy for the organisation +With this policy privacy relevant things can be configured (e.g. tos link) + + + + +### ResetPrivacyPolicyToDefault + +> **rpc** ResetPrivacyPolicyToDefault([ResetPrivacyPolicyToDefaultRequest](#resetprivacypolicytodefaultrequest)) +[ResetPrivacyPolicyToDefaultResponse](#resetprivacypolicytodefaultresponse) + +Removes the privacy policy of the organisation +The default policy of the IAM will trigger after + + + + ### GetLabelPolicy > **rpc** GetLabelPolicy([GetLabelPolicyRequest](#getlabelpolicyrequest)) @@ -2227,6 +2282,29 @@ This is an empty request +### AddCustomPrivacyPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| tos_link | string | - | | +| privacy_link | string | - | | + + + + +### AddCustomPrivacyPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### AddHumanUserRequest @@ -3239,6 +3317,23 @@ This is an empty request +### GetDefaultPrivacyPolicyRequest +This is an empty request + + + + +### GetDefaultPrivacyPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| policy | zitadel.policy.v1.PrivacyPolicy | - | | + + + + ### GetDefaultVerifyEmailMessageTextRequest @@ -3637,6 +3732,23 @@ This is an empty request +### GetPrivacyPolicyRequest +This is an empty request + + + + +### GetPrivacyPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| policy | zitadel.policy.v1.PrivacyPolicy | - | | + + + + ### GetProjectByIDRequest @@ -5607,6 +5719,23 @@ This is an empty request +### ResetPrivacyPolicyToDefaultRequest +This is an empty request + + + + +### ResetPrivacyPolicyToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SendHumanResetPasswordNotificationRequest @@ -6044,6 +6173,29 @@ This is an empty request +### UpdateCustomPrivacyPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| tos_link | string | - | | +| privacy_link | string | - | | + + + + +### UpdateCustomPrivacyPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateHumanEmailRequest diff --git a/docs/docs/apis/proto/policy.md b/docs/docs/apis/proto/policy.md index e2cef0fd26..056c6784f4 100644 --- a/docs/docs/apis/proto/policy.md +++ b/docs/docs/apis/proto/policy.md @@ -112,6 +112,20 @@ title: zitadel/policy.proto +### PrivacyPolicy + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| tos_link | string | - | | +| privacy_link | string | - | | +| is_default | bool | - | | + + + + ## Enums diff --git a/internal/admin/repository/eventsourcing/eventstore/iam.go b/internal/admin/repository/eventsourcing/eventstore/iam.go index 12be269857..fee3206379 100644 --- a/internal/admin/repository/eventsourcing/eventstore/iam.go +++ b/internal/admin/repository/eventsourcing/eventstore/iam.go @@ -372,6 +372,33 @@ func (repo *IAMRepository) GetDefaultMessageText(ctx context.Context, textType, return iam_es_model.MessageTextViewToModel(text), err } +func (repo *IAMRepository) GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) { + policy, viewErr := repo.View.PrivacyPolicyByAggregateID(repo.SystemDefaults.IamID) + if viewErr != nil && !caos_errs.IsNotFound(viewErr) { + return nil, viewErr + } + if caos_errs.IsNotFound(viewErr) { + policy = new(iam_es_model.PrivacyPolicyView) + } + events, esErr := repo.getIAMEvents(ctx, policy.Sequence) + if caos_errs.IsNotFound(viewErr) && len(events) == 0 { + return nil, caos_errs.ThrowNotFound(nil, "EVENT-84Nfs", "Errors.IAM.PrivacyPolicy.NotFound") + } + if esErr != nil { + logging.Log("EVENT-0p3Fs").WithError(esErr).Debug("error retrieving new events") + return iam_es_model.PrivacyViewToModel(policy), nil + } + policyCopy := *policy + for _, event := range events { + if err := policyCopy.AppendEvent(event); err != nil { + return iam_es_model.PrivacyViewToModel(policy), nil + } + } + result := iam_es_model.PrivacyViewToModel(policy) + result.Default = true + return result, nil +} + func (repo *IAMRepository) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) { query, err := iam_view.IAMByIDQuery(domain.IAMID, sequence) if err != nil { diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index 9695aeb756..62d2d4fe3b 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -66,6 +66,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es handler{view, bulkLimit, configs.cycleDuration("MessageText"), errorCount, es}), newFeatures( handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}), + newPrivacyPolicy( + handler{view, bulkLimit, configs.cycleDuration("PrivacyPolicy"), errorCount, es}), } if static != nil { handlers = append(handlers, newStyling( diff --git a/internal/admin/repository/eventsourcing/handler/privacy_policy.go b/internal/admin/repository/eventsourcing/handler/privacy_policy.go new file mode 100644 index 0000000000..9620de9f41 --- /dev/null +++ b/internal/admin/repository/eventsourcing/handler/privacy_policy.go @@ -0,0 +1,106 @@ +package handler + +import ( + "github.com/caos/logging" + "github.com/caos/zitadel/internal/eventstore/v1" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + + 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" + iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" +) + +const ( + privacyPolicyTable = "adminapi.privacy_policies" +) + +type PrivacyPolicy struct { + handler + subscription *v1.Subscription +} + +func newPrivacyPolicy(handler handler) *PrivacyPolicy { + h := &PrivacyPolicy{ + handler: handler, + } + + h.subscribe() + + return h +} + +func (p *PrivacyPolicy) subscribe() { + p.subscription = p.es.Subscribe(p.AggregateTypes()...) + go func() { + for event := range p.subscription.Events { + query.ReduceEvent(p, event) + } + }() +} + +func (p *PrivacyPolicy) ViewModel() string { + return privacyPolicyTable +} + +func (p *PrivacyPolicy) AggregateTypes() []es_models.AggregateType { + return []es_models.AggregateType{model.OrgAggregate, iam_es_model.IAMAggregate} +} + +func (p *PrivacyPolicy) CurrentSequence() (uint64, error) { + sequence, err := p.view.GetLatestPrivacyPolicySequence() + if err != nil { + return 0, err + } + return sequence.CurrentSequence, nil +} + +func (p *PrivacyPolicy) EventQuery() (*es_models.SearchQuery, error) { + sequence, err := p.view.GetLatestPrivacyPolicySequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(p.AggregateTypes()...). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (p *PrivacyPolicy) Reduce(event *es_models.Event) (err error) { + switch event.AggregateType { + case model.OrgAggregate, iam_es_model.IAMAggregate: + err = p.processPrivacyPolicy(event) + } + return err +} + +func (p *PrivacyPolicy) processPrivacyPolicy(event *es_models.Event) (err error) { + policy := new(iam_model.PrivacyPolicyView) + switch event.Type { + case iam_es_model.PrivacyPolicyAdded, model.PrivacyPolicyAdded: + err = policy.AppendEvent(event) + case iam_es_model.PrivacyPolicyChanged, model.PrivacyPolicyChanged: + policy, err = p.view.PrivacyPolicyByAggregateID(event.AggregateID) + if err != nil { + return err + } + err = policy.AppendEvent(event) + case model.PrivacyPolicyRemoved: + return p.view.DeletePrivacyPolicy(event.AggregateID, event) + default: + return p.view.ProcessedPrivacyPolicySequence(event) + } + if err != nil { + return err + } + return p.view.PutPrivacyPolicy(policy, event) +} + +func (p *PrivacyPolicy) OnError(event *es_models.Event, err error) error { + logging.LogWithFields("SPOOL-4N8sw", "id", event.AggregateID).WithError(err).Warn("something went wrong in privacy policy handler") + return spooler.HandleError(event, err, p.view.GetLatestPrivacyPolicyFailedEvent, p.view.ProcessedPrivacyPolicyFailedEvent, p.view.ProcessedPrivacyPolicySequence, p.errorCountUntilSkip) +} + +func (p *PrivacyPolicy) OnSuccess() error { + return spooler.HandleSuccess(p.view.UpdatePrivacyPolicySpoolerRunTimestamp) +} diff --git a/internal/admin/repository/eventsourcing/view/privacy_policy.go b/internal/admin/repository/eventsourcing/view/privacy_policy.go new file mode 100644 index 0000000000..8f3eded17f --- /dev/null +++ b/internal/admin/repository/eventsourcing/view/privacy_policy.go @@ -0,0 +1,53 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + privacyPolicyTable = "adminapi.privacy_policies" +) + +func (v *View) PrivacyPolicyByAggregateID(aggregateID string) (*model.PrivacyPolicyView, error) { + return view.GetPrivacyPolicyByAggregateID(v.Db, privacyPolicyTable, aggregateID) +} + +func (v *View) PutPrivacyPolicy(policy *model.PrivacyPolicyView, event *models.Event) error { + err := view.PutPrivacyPolicy(v.Db, privacyPolicyTable, policy) + if err != nil { + return err + } + return v.ProcessedPrivacyPolicySequence(event) +} + +func (v *View) DeletePrivacyPolicy(aggregateID string, event *models.Event) error { + err := view.DeletePrivacyPolicy(v.Db, privacyPolicyTable, aggregateID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedPrivacyPolicySequence(event) +} + +func (v *View) GetLatestPrivacyPolicySequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(privacyPolicyTable) +} + +func (v *View) ProcessedPrivacyPolicySequence(event *models.Event) error { + return v.saveCurrentSequence(privacyPolicyTable, event) +} + +func (v *View) UpdatePrivacyPolicySpoolerRunTimestamp() error { + return v.updateSpoolerRunSequence(privacyPolicyTable) +} + +func (v *View) GetLatestPrivacyPolicyFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(privacyPolicyTable, sequence) +} + +func (v *View) ProcessedPrivacyPolicyFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/admin/repository/iam.go b/internal/admin/repository/iam.go index 4bf9c0f653..676546dc71 100644 --- a/internal/admin/repository/iam.go +++ b/internal/admin/repository/iam.go @@ -37,5 +37,7 @@ type IAMRepository interface { GetDefaultPasswordLockoutPolicy(ctx context.Context) (*iam_model.PasswordLockoutPolicyView, error) + GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) + GetDefaultOrgIAMPolicy(ctx context.Context) (*iam_model.OrgIAMPolicyView, error) } diff --git a/internal/api/grpc/admin/features.go b/internal/api/grpc/admin/features.go index 21443cdb93..8e3be2a635 100644 --- a/internal/api/grpc/admin/features.go +++ b/internal/api/grpc/admin/features.go @@ -75,6 +75,7 @@ func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) LabelPolicyWatermark: req.LabelPolicyWatermark, CustomDomain: req.CustomDomain, CustomText: req.CustomText, + PrivacyPolicy: req.PrivacyPolicy, } } @@ -96,5 +97,6 @@ func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain. LabelPolicyWatermark: req.LabelPolicyWatermark, CustomDomain: req.CustomDomain, CustomText: req.CustomText, + PrivacyPolicy: req.PrivacyPolicy, } } diff --git a/internal/api/grpc/admin/privacy_policy.go b/internal/api/grpc/admin/privacy_policy.go new file mode 100644 index 0000000000..95497edf70 --- /dev/null +++ b/internal/api/grpc/admin/privacy_policy.go @@ -0,0 +1,31 @@ +package admin + +import ( + "context" + + "github.com/caos/zitadel/internal/api/grpc/object" + policy_grpc "github.com/caos/zitadel/internal/api/grpc/policy" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" +) + +func (s *Server) GetPrivacyPolicy(ctx context.Context, _ *admin_pb.GetPrivacyPolicyRequest) (*admin_pb.GetPrivacyPolicyResponse, error) { + policy, err := s.iam.GetDefaultPrivacyPolicy(ctx) + if err != nil { + return nil, err + } + return &admin_pb.GetPrivacyPolicyResponse{Policy: policy_grpc.ModelPrivacyPolicyToPb(policy)}, nil +} + +func (s *Server) UpdatePrivacyPolicy(ctx context.Context, req *admin_pb.UpdatePrivacyPolicyRequest) (*admin_pb.UpdatePrivacyPolicyResponse, error) { + result, err := s.command.ChangeDefaultPrivacyPolicy(ctx, UpdatePrivacyPolicyToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdatePrivacyPolicyResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.ChangeDate, + result.ResourceOwner, + ), + }, nil +} diff --git a/internal/api/grpc/admin/privacy_policy_converter.go b/internal/api/grpc/admin/privacy_policy_converter.go new file mode 100644 index 0000000000..2c2c9fb4cc --- /dev/null +++ b/internal/api/grpc/admin/privacy_policy_converter.go @@ -0,0 +1,13 @@ +package admin + +import ( + "github.com/caos/zitadel/internal/domain" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" +) + +func UpdatePrivacyPolicyToDomain(req *admin_pb.UpdatePrivacyPolicyRequest) *domain.PrivacyPolicy { + return &domain.PrivacyPolicy{ + TOSLink: req.TosLink, + PrivacyLink: req.PrivacyLink, + } +} diff --git a/internal/api/grpc/features/features.go b/internal/api/grpc/features/features.go index b2b47f9956..3b9a78dcd5 100644 --- a/internal/api/grpc/features/features.go +++ b/internal/api/grpc/features/features.go @@ -28,6 +28,7 @@ func FeaturesFromModel(features *features_model.FeaturesView) *features_pb.Featu LabelPolicyPrivateLabel: features.LabelPolicyPrivateLabel, LabelPolicyWatermark: features.LabelPolicyWatermark, CustomText: features.CustomText, + PrivacyPolicy: features.PrivacyPolicy, } } diff --git a/internal/api/grpc/management/policy_privacy.go b/internal/api/grpc/management/policy_privacy.go new file mode 100644 index 0000000000..45e0bcb556 --- /dev/null +++ b/internal/api/grpc/management/policy_privacy.go @@ -0,0 +1,64 @@ +package management + +import ( + "context" + + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/api/grpc/object" + policy_grpc "github.com/caos/zitadel/internal/api/grpc/policy" + mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" +) + +func (s *Server) GetPrivacyPolicy(ctx context.Context, _ *mgmt_pb.GetPrivacyPolicyRequest) (*mgmt_pb.GetPrivacyPolicyResponse, error) { + policy, err := s.org.GetPrivacyPolicy(ctx) + if err != nil { + return nil, err + } + return &mgmt_pb.GetPrivacyPolicyResponse{Policy: policy_grpc.ModelPrivacyPolicyToPb(policy)}, nil +} + +func (s *Server) GetDefaultPrivacyPolicy(ctx context.Context, _ *mgmt_pb.GetDefaultPrivacyPolicyRequest) (*mgmt_pb.GetDefaultPrivacyPolicyResponse, error) { + policy, err := s.org.GetDefaultPrivacyPolicy(ctx) + if err != nil { + return nil, err + } + return &mgmt_pb.GetDefaultPrivacyPolicyResponse{Policy: policy_grpc.ModelPrivacyPolicyToPb(policy)}, nil +} + +func (s *Server) AddCustomPrivacyPolicy(ctx context.Context, req *mgmt_pb.AddCustomPrivacyPolicyRequest) (*mgmt_pb.AddCustomPrivacyPolicyResponse, error) { + result, err := s.command.AddPrivacyPolicy(ctx, authz.GetCtxData(ctx).OrgID, AddPrivacyPolicyToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.AddCustomPrivacyPolicyResponse{ + Details: object.AddToDetailsPb( + result.Sequence, + result.ChangeDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) UpdateCustomPrivacyPolicy(ctx context.Context, req *mgmt_pb.UpdateCustomPrivacyPolicyRequest) (*mgmt_pb.UpdateCustomPrivacyPolicyResponse, error) { + result, err := s.command.ChangePrivacyPolicy(ctx, authz.GetCtxData(ctx).OrgID, UpdatePrivacyPolicyToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateCustomPrivacyPolicyResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.ChangeDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) ResetPrivacyPolicyToDefault(ctx context.Context, _ *mgmt_pb.ResetPrivacyPolicyToDefaultRequest) (*mgmt_pb.ResetPrivacyPolicyToDefaultResponse, error) { + objectDetails, err := s.command.RemovePrivacyPolicy(ctx, authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.ResetPrivacyPolicyToDefaultResponse{ + Details: object.DomainToChangeDetailsPb(objectDetails), + }, nil +} diff --git a/internal/api/grpc/management/policy_privacy_converter.go b/internal/api/grpc/management/policy_privacy_converter.go new file mode 100644 index 0000000000..f78c5ac12d --- /dev/null +++ b/internal/api/grpc/management/policy_privacy_converter.go @@ -0,0 +1,20 @@ +package management + +import ( + "github.com/caos/zitadel/internal/domain" + mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" +) + +func AddPrivacyPolicyToDomain(req *mgmt_pb.AddCustomPrivacyPolicyRequest) *domain.PrivacyPolicy { + return &domain.PrivacyPolicy{ + TOSLink: req.TosLink, + PrivacyLink: req.PrivacyLink, + } +} + +func UpdatePrivacyPolicyToDomain(req *mgmt_pb.UpdateCustomPrivacyPolicyRequest) *domain.PrivacyPolicy { + return &domain.PrivacyPolicy{ + TOSLink: req.TosLink, + PrivacyLink: req.PrivacyLink, + } +} diff --git a/internal/api/grpc/policy/privacy_policy.go b/internal/api/grpc/policy/privacy_policy.go new file mode 100644 index 0000000000..b3816c1bf5 --- /dev/null +++ b/internal/api/grpc/policy/privacy_policy.go @@ -0,0 +1,21 @@ +package policy + +import ( + "github.com/caos/zitadel/internal/api/grpc/object" + "github.com/caos/zitadel/internal/iam/model" + policy_pb "github.com/caos/zitadel/pkg/grpc/policy" +) + +func ModelPrivacyPolicyToPb(policy *model.PrivacyPolicyView) *policy_pb.PrivacyPolicy { + return &policy_pb.PrivacyPolicy{ + IsDefault: policy.Default, + TosLink: policy.TOSLink, + PrivacyLink: policy.PrivacyLink, + Details: object.ToViewDetailsPb( + policy.Sequence, + policy.CreationDate, + policy.ChangeDate, + "", //TODO: resourceowner + ), + } +} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 1775e6ce8e..707f7b5e73 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -440,6 +440,11 @@ func (repo *AuthRequestRepo) fillLoginPolicy(ctx context.Context, request *domai if idpProviders != nil { request.AllowedExternalIDPs = idpProviders } + privacyPolicy, err := repo.getPrivacyPolicy(ctx, orgID) + if err != nil { + return err + } + request.PrivacyPolicy = privacyPolicy labelPolicy, err := repo.getLabelPolicy(ctx, orgID) if err != nil { return err @@ -719,6 +724,21 @@ func (repo *AuthRequestRepo) getLoginPolicy(ctx context.Context, orgID string) ( return iam_es_model.LoginPolicyViewToModel(policy), err } +func (repo *AuthRequestRepo) getPrivacyPolicy(ctx context.Context, orgID string) (*domain.PrivacyPolicy, error) { + policy, err := repo.View.PrivacyPolicyByAggregateID(orgID) + if errors.IsNotFound(err) { + policy, err = repo.View.PrivacyPolicyByAggregateID(repo.IAMID) + if err != nil { + return nil, err + } + policy.Default = true + } + if err != nil { + return nil, err + } + return policy.ToDomain(), err +} + func (repo *AuthRequestRepo) getLabelPolicy(ctx context.Context, orgID string) (*domain.LabelPolicy, error) { policy, err := repo.View.LabelPolicyByAggregateIDAndState(orgID, int32(domain.LabelPolicyStateActive)) if errors.IsNotFound(err) { diff --git a/internal/auth/repository/eventsourcing/eventstore/org.go b/internal/auth/repository/eventsourcing/eventstore/org.go index f0c85eb9fc..84692e3042 100644 --- a/internal/auth/repository/eventsourcing/eventstore/org.go +++ b/internal/auth/repository/eventsourcing/eventstore/org.go @@ -115,3 +115,11 @@ func (repo *OrgRepository) GetLabelPolicy(ctx context.Context, orgID string) (*i } return iam_view_model.LabelPolicyViewToModel(orgPolicy), nil } + +func (repo *OrgRepository) GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) { + policy, err := repo.View.PrivacyPolicyByAggregateID(repo.SystemDefaults.IamID) + if err != nil { + return nil, err + } + return iam_view_model.PrivacyViewToModel(policy), nil +} diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 0f60096f07..580387da7f 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -70,6 +70,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es newLabelPolicy(handler{view, bulkLimit, configs.cycleDuration("LabelPolicy"), errorCount, es}), newFeatures(handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}), newRefreshToken(handler{view, bulkLimit, configs.cycleDuration("RefreshToken"), errorCount, es}), + newPrivacyPolicy(handler{view, bulkLimit, configs.cycleDuration("PrivacyPolicy"), errorCount, es}), } } diff --git a/internal/auth/repository/eventsourcing/handler/privacy_policy.go b/internal/auth/repository/eventsourcing/handler/privacy_policy.go new file mode 100644 index 0000000000..f79a036dd9 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/privacy_policy.go @@ -0,0 +1,106 @@ +package handler + +import ( + "github.com/caos/logging" + "github.com/caos/zitadel/internal/eventstore/v1" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + + 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" + iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" +) + +const ( + privacyPolicyTable = "auth.privacy_policies" +) + +type PrivacyPolicy struct { + handler + subscription *v1.Subscription +} + +func newPrivacyPolicy(handler handler) *PrivacyPolicy { + h := &PrivacyPolicy{ + handler: handler, + } + + h.subscribe() + + return h +} + +func (p *PrivacyPolicy) subscribe() { + p.subscription = p.es.Subscribe(p.AggregateTypes()...) + go func() { + for event := range p.subscription.Events { + query.ReduceEvent(p, event) + } + }() +} + +func (p *PrivacyPolicy) ViewModel() string { + return privacyPolicyTable +} + +func (p *PrivacyPolicy) AggregateTypes() []es_models.AggregateType { + return []es_models.AggregateType{model.OrgAggregate, iam_es_model.IAMAggregate} +} + +func (p *PrivacyPolicy) CurrentSequence() (uint64, error) { + sequence, err := p.view.GetLatestPrivacyPolicySequence() + if err != nil { + return 0, err + } + return sequence.CurrentSequence, nil +} + +func (p *PrivacyPolicy) EventQuery() (*es_models.SearchQuery, error) { + sequence, err := p.view.GetLatestPrivacyPolicySequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(p.AggregateTypes()...). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (p *PrivacyPolicy) Reduce(event *es_models.Event) (err error) { + switch event.AggregateType { + case model.OrgAggregate, iam_es_model.IAMAggregate: + err = p.processPrivacyPolicy(event) + } + return err +} + +func (p *PrivacyPolicy) processPrivacyPolicy(event *es_models.Event) (err error) { + policy := new(iam_model.PrivacyPolicyView) + switch event.Type { + case iam_es_model.PrivacyPolicyAdded, model.PrivacyPolicyAdded: + err = policy.AppendEvent(event) + case iam_es_model.PrivacyPolicyChanged, model.PrivacyPolicyChanged: + policy, err = p.view.PrivacyPolicyByAggregateID(event.AggregateID) + if err != nil { + return err + } + err = policy.AppendEvent(event) + case model.PrivacyPolicyRemoved: + return p.view.DeletePrivacyPolicy(event.AggregateID, event) + default: + return p.view.ProcessedPrivacyPolicySequence(event) + } + if err != nil { + return err + } + return p.view.PutPrivacyPolicy(policy, event) +} + +func (p *PrivacyPolicy) OnError(event *es_models.Event, err error) error { + logging.LogWithFields("SPOOL-4N8sw", "id", event.AggregateID).WithError(err).Warn("something went wrong in privacy policy handler") + return spooler.HandleError(event, err, p.view.GetLatestPrivacyPolicyFailedEvent, p.view.ProcessedPrivacyPolicyFailedEvent, p.view.ProcessedPrivacyPolicySequence, p.errorCountUntilSkip) +} + +func (p *PrivacyPolicy) OnSuccess() error { + return spooler.HandleSuccess(p.view.UpdatePrivacyPolicySpoolerRunTimestamp) +} diff --git a/internal/auth/repository/eventsourcing/view/privacy_policy.go b/internal/auth/repository/eventsourcing/view/privacy_policy.go new file mode 100644 index 0000000000..8a2e921273 --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/privacy_policy.go @@ -0,0 +1,53 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + privacyPolicyTable = "auth.privacy_policies" +) + +func (v *View) PrivacyPolicyByAggregateID(aggregateID string) (*model.PrivacyPolicyView, error) { + return view.GetPrivacyPolicyByAggregateID(v.Db, privacyPolicyTable, aggregateID) +} + +func (v *View) PutPrivacyPolicy(policy *model.PrivacyPolicyView, event *models.Event) error { + err := view.PutPrivacyPolicy(v.Db, privacyPolicyTable, policy) + if err != nil { + return err + } + return v.ProcessedPrivacyPolicySequence(event) +} + +func (v *View) DeletePrivacyPolicy(aggregateID string, event *models.Event) error { + err := view.DeletePrivacyPolicy(v.Db, privacyPolicyTable, aggregateID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedPrivacyPolicySequence(event) +} + +func (v *View) GetLatestPrivacyPolicySequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(privacyPolicyTable) +} + +func (v *View) ProcessedPrivacyPolicySequence(event *models.Event) error { + return v.saveCurrentSequence(privacyPolicyTable, event) +} + +func (v *View) UpdatePrivacyPolicySpoolerRunTimestamp() error { + return v.updateSpoolerRunSequence(privacyPolicyTable) +} + +func (v *View) GetLatestPrivacyPolicyFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(privacyPolicyTable, sequence) +} + +func (v *View) ProcessedPrivacyPolicyFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/org.go b/internal/auth/repository/org.go index c3e2d21393..934c4ef653 100644 --- a/internal/auth/repository/org.go +++ b/internal/auth/repository/org.go @@ -13,4 +13,5 @@ type OrgRepository interface { GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error) GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error) GetLabelPolicy(ctx context.Context, orgID string) (*iam_model.LabelPolicyView, error) + GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 862e298678..29b8f5bed5 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -157,6 +157,12 @@ func checkFeatures(features *features_view_model.FeaturesView, requiredFeatures } continue } + if requiredFeature == domain.FeaturePrivacyPolicy { + if !features.PrivacyPolicy { + return MissingFeatureErr(requiredFeature) + } + continue + } return MissingFeatureErr(requiredFeature) } return nil diff --git a/internal/command/features_model.go b/internal/command/features_model.go index 62567a6f2e..5878b6faad 100644 --- a/internal/command/features_model.go +++ b/internal/command/features_model.go @@ -27,6 +27,7 @@ type FeaturesWriteModel struct { LabelPolicyWatermark bool CustomDomain bool CustomText bool + PrivacyPolicy bool } func (wm *FeaturesWriteModel) Reduce() error { diff --git a/internal/command/iam_converter.go b/internal/command/iam_converter.go index f1fcf3111a..cbe41a3c7c 100644 --- a/internal/command/iam_converter.go +++ b/internal/command/iam_converter.go @@ -120,6 +120,14 @@ func writeModelToPasswordLockoutPolicy(wm *PasswordLockoutPolicyWriteModel) *dom } } +func writeModelToPrivacyPolicy(wm *PrivacyPolicyWriteModel) *domain.PrivacyPolicy { + return &domain.PrivacyPolicy{ + ObjectRoot: writeModelToObjectRoot(wm.WriteModel), + TOSLink: wm.TOSLink, + PrivacyLink: wm.PrivacyLink, + } +} + func writeModelToIDPConfig(wm *IDPConfigWriteModel) *domain.IDPConfig { return &domain.IDPConfig{ ObjectRoot: writeModelToObjectRoot(wm.WriteModel), diff --git a/internal/command/iam_features.go b/internal/command/iam_features.go index c24eebfa96..afdb3fee6e 100644 --- a/internal/command/iam_features.go +++ b/internal/command/iam_features.go @@ -50,6 +50,7 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAM features.LabelPolicyWatermark, features.CustomDomain, features.CustomText, + features.PrivacyPolicy, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") diff --git a/internal/command/iam_features_model.go b/internal/command/iam_features_model.go index 2a043bc851..11007bfbc1 100644 --- a/internal/command/iam_features_model.go +++ b/internal/command/iam_features_model.go @@ -65,7 +65,8 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( labelPolicyPrivateLabel, labelPolicyWatermark, customDomain, - customText bool, + customText, + privacyPolicy bool, ) (*iam.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -115,7 +116,9 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( if wm.CustomText != customText { changes = append(changes, features.ChangeCustomText(customText)) } - + if wm.PrivacyPolicy != privacyPolicy { + changes = append(changes, features.ChangePrivacyPolicy(privacyPolicy)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/iam_policy_privacy.go b/internal/command/iam_policy_privacy.go new file mode 100644 index 0000000000..3b3eb19cc0 --- /dev/null +++ b/internal/command/iam_policy_privacy.go @@ -0,0 +1,92 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + iam_repo "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/telemetry/tracing" +) + +func (c *Commands) getDefaultPrivacyPolicy(ctx context.Context) (*domain.PrivacyPolicy, error) { + policyWriteModel := NewIAMPrivacyPolicyWriteModel() + err := c.eventstore.FilterToQueryReducer(ctx, policyWriteModel) + if err != nil { + return nil, err + } + if !policyWriteModel.State.Exists() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-559os", "Errors.IAM.OrgIAMPolicy.NotFound") + } + policy := writeModelToPrivacyPolicy(&policyWriteModel.PrivacyPolicyWriteModel) + policy.Default = true + return policy, nil +} + +func (c *Commands) AddDefaultPrivacyPolicy(ctx context.Context, policy *domain.PrivacyPolicy) (*domain.PrivacyPolicy, error) { + addedPolicy := NewIAMPrivacyPolicyWriteModel() + iamAgg := IAMAggregateFromWriteModel(&addedPolicy.WriteModel) + events, err := c.addDefaultPrivacyPolicy(ctx, iamAgg, addedPolicy, policy) + if err != nil { + return nil, err + } + + pushedEvents, err := c.eventstore.PushEvents(ctx, events) + if err != nil { + return nil, err + } + err = AppendAndReduce(addedPolicy, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToPrivacyPolicy(&addedPolicy.PrivacyPolicyWriteModel), nil +} + +func (c *Commands) addDefaultPrivacyPolicy(ctx context.Context, iamAgg *eventstore.Aggregate, addedPolicy *IAMPrivacyPolicyWriteModel, policy *domain.PrivacyPolicy) (eventstore.EventPusher, error) { + err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) + if err != nil { + return nil, err + } + if addedPolicy.State == domain.PolicyStateActive { + return nil, caos_errs.ThrowAlreadyExists(nil, "IAM-M00rJ", "Errors.IAM.PrivacyPolicy.AlreadyExists") + } + + return iam_repo.NewPrivacyPolicyAddedEvent(ctx, iamAgg, policy.TOSLink, policy.PrivacyLink), nil +} + +func (c *Commands) ChangeDefaultPrivacyPolicy(ctx context.Context, policy *domain.PrivacyPolicy) (*domain.PrivacyPolicy, error) { + existingPolicy, err := c.defaultPrivacyPolicyWriteModelByID(ctx) + if err != nil { + return nil, err + } + if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "IAM-0oPew", "Errors.IAM.PasswordAgePolicy.NotFound") + } + + iamAgg := IAMAggregateFromWriteModel(&existingPolicy.PrivacyPolicyWriteModel.WriteModel) + changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, iamAgg, policy.TOSLink, policy.PrivacyLink) + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-4M9vs", "Errors.IAM.LabelPolicy.NotChanged") + } + pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingPolicy, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToPrivacyPolicy(&existingPolicy.PrivacyPolicyWriteModel), nil +} + +func (c *Commands) defaultPrivacyPolicyWriteModelByID(ctx context.Context) (policy *IAMPrivacyPolicyWriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + writeModel := NewIAMPrivacyPolicyWriteModel() + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/iam_policy_privacy_model.go b/internal/command/iam_policy_privacy_model.go new file mode 100644 index 0000000000..ef3daadcbb --- /dev/null +++ b/internal/command/iam_policy_privacy_model.go @@ -0,0 +1,73 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" +) + +type IAMPrivacyPolicyWriteModel struct { + PrivacyPolicyWriteModel +} + +func NewIAMPrivacyPolicyWriteModel() *IAMPrivacyPolicyWriteModel { + return &IAMPrivacyPolicyWriteModel{ + PrivacyPolicyWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: domain.IAMID, + ResourceOwner: domain.IAMID, + }, + }, + } +} + +func (wm *IAMPrivacyPolicyWriteModel) AppendEvents(events ...eventstore.EventReader) { + for _, event := range events { + switch e := event.(type) { + case *iam.PrivacyPolicyAddedEvent: + wm.PrivacyPolicyWriteModel.AppendEvents(&e.PrivacyPolicyAddedEvent) + case *iam.PrivacyPolicyChangedEvent: + wm.PrivacyPolicyWriteModel.AppendEvents(&e.PrivacyPolicyChangedEvent) + } + } +} + +func (wm *IAMPrivacyPolicyWriteModel) Reduce() error { + return wm.PrivacyPolicyWriteModel.Reduce() +} + +func (wm *IAMPrivacyPolicyWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType). + AggregateIDs(wm.PrivacyPolicyWriteModel.AggregateID). + ResourceOwner(wm.ResourceOwner). + EventTypes( + iam.PrivacyPolicyAddedEventType, + iam.PrivacyPolicyChangedEventType) +} + +func (wm *IAMPrivacyPolicyWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + tosLink, + privacyLink string, +) (*iam.PrivacyPolicyChangedEvent, bool) { + + changes := make([]policy.PrivacyPolicyChanges, 0) + if wm.TOSLink != tosLink { + changes = append(changes, policy.ChangeTOSLink(tosLink)) + } + if wm.PrivacyLink != privacyLink { + changes = append(changes, policy.ChangePrivacyLink(privacyLink)) + } + if len(changes) == 0 { + return nil, false + } + changedEvent, err := iam.NewPrivacyPolicyChangedEvent(ctx, aggregate, changes) + if err != nil { + return nil, false + } + return changedEvent, true +} diff --git a/internal/command/iam_policy_privacy_test.go b/internal/command/iam_policy_privacy_test.go new file mode 100644 index 0000000000..8c0a2eaf5a --- /dev/null +++ b/internal/command/iam_policy_privacy_test.go @@ -0,0 +1,292 @@ +package command + +import ( + "context" + "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/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PrivacyPolicy + } + type res struct { + want *domain.PrivacyPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "privacy policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + want: &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + }, + { + name: "add empty policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "", + "", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "", + PrivacyLink: "", + }, + }, + res: res{ + want: &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + TOSLink: "", + PrivacyLink: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultPrivacyPolicy(tt.args.ctx, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PrivacyPolicy + } + type res struct { + want *domain.PrivacyPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "privacy policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultPrivacyPolicyChangedEvent(context.Background(), + "TOSLinkChanged", + "PrivacyLinkChanged", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLinkChanged", + PrivacyLink: "PrivacyLinkChanged", + }, + }, + res: res{ + want: &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + TOSLink: "TOSLinkChanged", + PrivacyLink: "PrivacyLinkChanged", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultPrivacyPolicy(tt.args.ctx, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newDefaultPrivacyPolicyChangedEvent(ctx context.Context, tosLink, privacyLink string) *iam.PrivacyPolicyChangedEvent { + event, _ := iam.NewPrivacyPolicyChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.PrivacyPolicyChanges{ + policy.ChangeTOSLink(tosLink), + policy.ChangePrivacyLink(privacyLink), + }, + ) + return event +} diff --git a/internal/command/org_converter.go b/internal/command/org_converter.go index 9564df1998..f149d3fee7 100644 --- a/internal/command/org_converter.go +++ b/internal/command/org_converter.go @@ -41,3 +41,11 @@ func orgDomainWriteModelToOrgDomain(wm *OrgDomainWriteModel) *domain.OrgDomain { ValidationCode: wm.ValidationCode, } } + +func orgWriteModelToPrivacyPolicy(wm *OrgPrivacyPolicyWriteModel) *domain.PrivacyPolicy { + return &domain.PrivacyPolicy{ + ObjectRoot: writeModelToObjectRoot(wm.PrivacyPolicyWriteModel.WriteModel), + TOSLink: wm.TOSLink, + PrivacyLink: wm.PrivacyLink, + } +} diff --git a/internal/command/org_features.go b/internal/command/org_features.go index 85be241c1f..9f57e9d7f5 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -41,6 +41,7 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea features.LabelPolicyWatermark, features.CustomDomain, features.CustomText, + features.PrivacyPolicy, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") @@ -136,6 +137,15 @@ func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string events = append(events, removeCustomTextEvents...) } } + if !features.PrivacyPolicy { + removePrivacyPolicyEvent, err := c.removePrivacyPolicyIfExists(ctx, orgID) + if err != nil { + return nil, err + } + if removePrivacyPolicyEvent != nil { + events = append(events, removePrivacyPolicyEvent) + } + } return events, nil } diff --git a/internal/command/org_features_model.go b/internal/command/org_features_model.go index fc40a496fb..06f9145a0d 100644 --- a/internal/command/org_features_model.go +++ b/internal/command/org_features_model.go @@ -72,7 +72,8 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( labelPolicyPrivateLabel, labelPolicyWatermark, customDomain, - customText bool, + customText, + privacyPolicy bool, ) (*org.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -125,6 +126,9 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( if wm.CustomText != customText { changes = append(changes, features.ChangeCustomText(customText)) } + if wm.PrivacyPolicy != privacyPolicy { + changes = append(changes, features.ChangePrivacyPolicy(privacyPolicy)) + } if len(changes) == 0 { return nil, false diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go index f902c1f438..86f845bdbe 100644 --- a/internal/command/org_features_test.go +++ b/internal/command/org_features_test.go @@ -239,6 +239,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "toslink", + "privacylink", + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -267,6 +277,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LabelPolicyWatermark: false, CustomDomain: false, CustomText: false, + PrivacyPolicy: false, }, }, res: res{ @@ -399,6 +410,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "toslink", + "privacylink", + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -572,6 +593,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "toslink", + "privacylink", + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -755,6 +786,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "toslink", + "privacylink", + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -993,6 +1034,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "toslink", + "privacylink", + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -1016,6 +1067,9 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { eventFromEventPusher( org.NewCustomTextTemplateRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, domain.InitCodeMessageType, language.English), ), + eventFromEventPusher( + org.NewPrivacyPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), + ), eventFromEventPusher( newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour), ), @@ -1221,6 +1275,16 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewPrivacyPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "toslink", + "privacylink", + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( diff --git a/internal/command/org_policy_privacy.go b/internal/command/org_policy_privacy.go new file mode 100644 index 0000000000..bfa75d2c00 --- /dev/null +++ b/internal/command/org_policy_privacy.go @@ -0,0 +1,135 @@ +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) getOrgPrivacyPolicy(ctx context.Context, orgID string) (*domain.PrivacyPolicy, error) { + policy, err := c.orgPrivacyPolicyWriteModelByID(ctx, orgID) + if err != nil { + return nil, err + } + if policy.State == domain.PolicyStateActive { + return orgWriteModelToPrivacyPolicy(policy), nil + } + return c.getDefaultPrivacyPolicy(ctx) +} + +func (c *Commands) orgPrivacyPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgPrivacyPolicyWriteModel, error) { + policy := NewOrgPrivacyPolicyWriteModel(orgID) + err := c.eventstore.FilterToQueryReducer(ctx, policy) + if err != nil { + return nil, err + } + return policy, nil +} + +func (c *Commands) AddPrivacyPolicy(ctx context.Context, resourceOwner string, policy *domain.PrivacyPolicy) (*domain.PrivacyPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-MMk9fs", "Errors.ResourceOwnerMissing") + } + addedPolicy := NewOrgPrivacyPolicyWriteModel(resourceOwner) + err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) + if err != nil { + return nil, err + } + if addedPolicy.State == domain.PolicyStateActive { + return nil, caos_errs.ThrowAlreadyExists(nil, "Org-0oLpd", "Errors.Org.PrivacyPolicy.AlreadyExists") + } + + orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel) + pushedEvents, err := c.eventstore.PushEvents( + ctx, + org.NewPrivacyPolicyAddedEvent( + ctx, + orgAgg, + policy.TOSLink, + policy.PrivacyLink)) + if err != nil { + return nil, err + } + err = AppendAndReduce(addedPolicy, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToPrivacyPolicy(&addedPolicy.PrivacyPolicyWriteModel), nil +} + +func (c *Commands) ChangePrivacyPolicy(ctx context.Context, resourceOwner string, policy *domain.PrivacyPolicy) (*domain.PrivacyPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-22N89f", "Errors.ResourceOwnerMissing") + } + + existingPolicy := NewOrgPrivacyPolicyWriteModel(resourceOwner) + err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + if err != nil { + return nil, err + } + if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "ORG-Ng8sf", "Errors.Org.PrivacyPolicy.NotFound") + } + + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.PrivacyPolicyWriteModel.WriteModel) + changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.TOSLink, policy.PrivacyLink) + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-4N9fs", "Errors.Org.PrivacyPolicy.NotChanged") + } + + pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingPolicy, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToPrivacyPolicy(&existingPolicy.PrivacyPolicyWriteModel), nil +} + +func (c *Commands) RemovePrivacyPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { + if orgID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Nf9sf", "Errors.ResourceOwnerMissing") + } + existingPolicy := NewOrgPrivacyPolicyWriteModel(orgID) + event, err := c.removePrivacyPolicy(ctx, existingPolicy) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.PushEvents(ctx, event) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingPolicy, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingPolicy.PrivacyPolicyWriteModel.WriteModel), nil +} + +func (c *Commands) removePrivacyPolicy(ctx context.Context, existingPolicy *OrgPrivacyPolicyWriteModel) (*org.PrivacyPolicyRemovedEvent, error) { + err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + if err != nil { + return nil, err + } + if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "ORG-Ze9gs", "Errors.Org.PrivacyPolicy.NotFound") + } + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) + return org.NewPrivacyPolicyRemovedEvent(ctx, orgAgg), nil +} + +func (c *Commands) removePrivacyPolicyIfExists(ctx context.Context, orgID string) (*org.PrivacyPolicyRemovedEvent, error) { + existingPolicy, err := c.orgPrivacyPolicyWriteModelByID(ctx, orgID) + if err != nil { + return nil, err + } + if existingPolicy.State != domain.PolicyStateActive { + return nil, nil + } + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) + return org.NewPrivacyPolicyRemovedEvent(ctx, orgAgg), nil +} diff --git a/internal/command/org_policy_privacy_model.go b/internal/command/org_policy_privacy_model.go new file mode 100644 index 0000000000..d4a0384e17 --- /dev/null +++ b/internal/command/org_policy_privacy_model.go @@ -0,0 +1,74 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +type OrgPrivacyPolicyWriteModel struct { + PrivacyPolicyWriteModel +} + +func NewOrgPrivacyPolicyWriteModel(orgID string) *OrgPrivacyPolicyWriteModel { + return &OrgPrivacyPolicyWriteModel{ + PrivacyPolicyWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + }, + } +} + +func (wm *OrgPrivacyPolicyWriteModel) AppendEvents(events ...eventstore.EventReader) { + for _, event := range events { + switch e := event.(type) { + case *org.PrivacyPolicyAddedEvent: + wm.PrivacyPolicyWriteModel.AppendEvents(&e.PrivacyPolicyAddedEvent) + case *org.PrivacyPolicyChangedEvent: + wm.PrivacyPolicyWriteModel.AppendEvents(&e.PrivacyPolicyChangedEvent) + case *org.PrivacyPolicyRemovedEvent: + wm.PrivacyPolicyWriteModel.AppendEvents(&e.PrivacyPolicyRemovedEvent) + } + } +} + +func (wm *OrgPrivacyPolicyWriteModel) Reduce() error { + return wm.PrivacyPolicyWriteModel.Reduce() +} + +func (wm *OrgPrivacyPolicyWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType). + AggregateIDs(wm.PrivacyPolicyWriteModel.AggregateID). + ResourceOwner(wm.ResourceOwner). + EventTypes(org.PrivacyPolicyAddedEventType, + org.PrivacyPolicyChangedEventType, + org.PrivacyPolicyRemovedEventType) +} + +func (wm *OrgPrivacyPolicyWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + tosLink, + privacyLink string, +) (*org.PrivacyPolicyChangedEvent, bool) { + + changes := make([]policy.PrivacyPolicyChanges, 0) + if wm.TOSLink != tosLink { + changes = append(changes, policy.ChangeTOSLink(tosLink)) + } + if wm.PrivacyLink != privacyLink { + changes = append(changes, policy.ChangePrivacyLink(privacyLink)) + } + if len(changes) == 0 { + return nil, false + } + changedEvent, err := org.NewPrivacyPolicyChangedEvent(ctx, aggregate, changes) + if err != nil { + return nil, false + } + return changedEvent, true +} diff --git a/internal/command/org_policy_privacy_test.go b/internal/command/org_policy_privacy_test.go new file mode 100644 index 0000000000..ba762371cb --- /dev/null +++ b/internal/command/org_policy_privacy_test.go @@ -0,0 +1,479 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "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/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddPrivacyPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PrivacyPolicy + } + type res struct { + want *domain.PrivacyPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + want: &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + }, + { + name: "add policy empty links, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "", + "", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "", + PrivacyLink: "", + }, + }, + res: res{ + want: &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + TOSLink: "", + PrivacyLink: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddPrivacyPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PrivacyPolicy + } + type res struct { + want *domain.PrivacyPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newPrivacyPolicyChangedEvent(context.Background(), "org1", "TOSLinkChange", "PrivacyLinkChange"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLinkChange", + PrivacyLink: "PrivacyLinkChange", + }, + }, + res: res{ + want: &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + TOSLink: "TOSLinkChange", + PrivacyLink: "PrivacyLinkChange", + }, + }, + }, + { + name: "change to empty links, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newPrivacyPolicyChangedEvent(context.Background(), "org1", "", ""), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "", + PrivacyLink: "", + }, + }, + res: res{ + want: &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + TOSLink: "", + PrivacyLink: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangePrivacyPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemovePrivacyPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPrivacyPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "TOSLink", + "PrivacyLink", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPrivacyPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemovePrivacyPolicy(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newPrivacyPolicyChangedEvent(ctx context.Context, orgID string, tosLink, privacyLink string) *org.PrivacyPolicyChangedEvent { + event, _ := org.NewPrivacyPolicyChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.PrivacyPolicyChanges{ + policy.ChangeTOSLink(tosLink), + policy.ChangePrivacyLink(privacyLink), + }, + ) + return event +} diff --git a/internal/command/policy_privacy_model.go b/internal/command/policy_privacy_model.go new file mode 100644 index 0000000000..07e489f447 --- /dev/null +++ b/internal/command/policy_privacy_model.go @@ -0,0 +1,36 @@ +package command + +import ( + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/policy" +) + +type PrivacyPolicyWriteModel struct { + eventstore.WriteModel + + TOSLink string + PrivacyLink string + State domain.PolicyState +} + +func (wm *PrivacyPolicyWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *policy.PrivacyPolicyAddedEvent: + wm.TOSLink = e.TOSLink + wm.PrivacyLink = e.PrivacyLink + wm.State = domain.PolicyStateActive + case *policy.PrivacyPolicyChangedEvent: + if e.PrivacyLink != nil { + wm.PrivacyLink = *e.PrivacyLink + } + if e.TOSLink != nil { + wm.TOSLink = *e.TOSLink + } + case *policy.PrivacyPolicyRemovedEvent: + wm.State = domain.PolicyStateRemoved + } + } + return wm.WriteModel.Reduce() +} diff --git a/internal/command/setup_step17.go b/internal/command/setup_step17.go new file mode 100644 index 0000000000..63d3e3d9e3 --- /dev/null +++ b/internal/command/setup_step17.go @@ -0,0 +1,35 @@ +package command + +import ( + "context" + "github.com/caos/logging" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" +) + +type Step17 struct { + PrivacyPolicy domain.PrivacyPolicy +} + +func (s *Step17) Step() domain.Step { + return domain.Step17 +} + +func (s *Step17) execute(ctx context.Context, commandSide *Commands) error { + return commandSide.SetupStep17(ctx, s) +} + +func (c *Commands) SetupStep17(ctx context.Context, step *Step17) error { + fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) { + iamAgg := IAMAggregateFromWriteModel(&iam.WriteModel) + addedPolicy := NewIAMPrivacyPolicyWriteModel() + events, err := c.addDefaultPrivacyPolicy(ctx, iamAgg, addedPolicy, &step.PrivacyPolicy) + if err != nil { + return nil, err + } + + logging.Log("SETUP-N9sq2").Info("default privacy policy set up") + return []eventstore.EventPusher{events}, nil + } + return c.setup(ctx, step, fn) +} diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 21ea1aba18..f2c4eb923c 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -47,6 +47,7 @@ type AuthRequest struct { LoginPolicy *LoginPolicy AllowedExternalIDPs []*IDPProvider LabelPolicy *LabelPolicy + PrivacyPolicy *PrivacyPolicy } type ExternalUser struct { diff --git a/internal/domain/features.go b/internal/domain/features.go index ff49486e19..b07f35dfe6 100644 --- a/internal/domain/features.go +++ b/internal/domain/features.go @@ -20,6 +20,7 @@ const ( FeatureLabelPolicyWatermark = FeatureLabelPolicy + ".watermark" FeatureCustomText = "custom_text" FeatureCustomDomain = "custom_domain" + FeaturePrivacyPolicy = "privacy_policy" ) type Features struct { @@ -43,6 +44,7 @@ type Features struct { LabelPolicyWatermark bool CustomDomain bool CustomText bool + PrivacyPolicy bool } type FeaturesState int32 diff --git a/internal/domain/policy_privacy.go b/internal/domain/policy_privacy.go new file mode 100644 index 0000000000..9eb58f3897 --- /dev/null +++ b/internal/domain/policy_privacy.go @@ -0,0 +1,15 @@ +package domain + +import ( + "github.com/caos/zitadel/internal/eventstore/v1/models" +) + +type PrivacyPolicy struct { + models.ObjectRoot + + State PolicyState + Default bool + + TOSLink string + PrivacyLink string +} diff --git a/internal/domain/step.go b/internal/domain/step.go index f93642cf8f..9ea39ca770 100644 --- a/internal/domain/step.go +++ b/internal/domain/step.go @@ -19,6 +19,7 @@ const ( Step14 Step15 Step16 + Step17 //StepCount marks the the length of possible steps (StepCount-1 == last possible step) StepCount ) diff --git a/internal/features/model/features_view.go b/internal/features/model/features_view.go index d2fdf24c3e..4cf7519a2b 100644 --- a/internal/features/model/features_view.go +++ b/internal/features/model/features_view.go @@ -29,6 +29,7 @@ type FeaturesView struct { LabelPolicyWatermark bool CustomDomain bool CustomText bool + PrivacyPolicy bool } func (f *FeaturesView) FeatureList() []string { @@ -66,6 +67,9 @@ func (f *FeaturesView) FeatureList() []string { if f.CustomText { list = append(list, domain.FeatureCustomText) } + if f.PrivacyPolicy { + list = append(list, domain.FeaturePrivacyPolicy) + } return list } diff --git a/internal/features/repository/view/model/features.go b/internal/features/repository/view/model/features.go index ca8dc1af38..b8fe4819ff 100644 --- a/internal/features/repository/view/model/features.go +++ b/internal/features/repository/view/model/features.go @@ -43,6 +43,7 @@ type FeaturesView struct { LabelPolicyWatermark bool `json:"labelPolicyWatermark" gorm:"column:label_policy_watermark"` CustomDomain bool `json:"customDomain" gorm:"column:custom_domain"` CustomText bool `json:"customText" gorm:"column:custom_text"` + PrivacyPolicy bool `json:"privacyPolicy" gorm:"column:privacy_policy"` } func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { @@ -68,6 +69,7 @@ func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { LabelPolicyWatermark: features.LabelPolicyWatermark, CustomDomain: features.CustomDomain, CustomText: features.CustomText, + PrivacyPolicy: features.PrivacyPolicy, } } diff --git a/internal/iam/model/privacy_policy_view.go b/internal/iam/model/privacy_policy_view.go new file mode 100644 index 0000000000..d0bcce3b1c --- /dev/null +++ b/internal/iam/model/privacy_policy_view.go @@ -0,0 +1,48 @@ +package model + +import ( + "time" + + "github.com/caos/zitadel/internal/domain" +) + +type PrivacyPolicyView struct { + AggregateID string + TOSLink string + PrivacyLink string + Default bool + + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 +} + +type PrivacyPolicySearchRequest struct { + Offset uint64 + Limit uint64 + SortingColumn PrivacyPolicySearchKey + Asc bool + Queries []*PrivacyPolicySearchQuery +} + +type PrivacyPolicySearchKey int32 + +const ( + PrivacyPolicySearchKeyUnspecified PrivacyPolicySearchKey = iota + PrivacyPolicySearchKeyAggregateID +) + +type PrivacyPolicySearchQuery struct { + Key PrivacyPolicySearchKey + Method domain.SearchMethod + Value interface{} +} + +type PrivacyPolicySearchResponse struct { + Offset uint64 + Limit uint64 + TotalResult uint64 + Result []*PrivacyPolicyView + Sequence uint64 + Timestamp time.Time +} diff --git a/internal/iam/repository/eventsourcing/model/types.go b/internal/iam/repository/eventsourcing/model/types.go index d538aaed52..2f7d431cb7 100644 --- a/internal/iam/repository/eventsourcing/model/types.go +++ b/internal/iam/repository/eventsourcing/model/types.go @@ -67,6 +67,9 @@ const ( PasswordLockoutPolicyAdded models.EventType = "iam.policy.password.lockout.added" PasswordLockoutPolicyChanged models.EventType = "iam.policy.password.lockout.changed" + PrivacyPolicyAdded models.EventType = "iam.policy.privacy.added" + PrivacyPolicyChanged models.EventType = "iam.policy.privacy.changed" + OrgIAMPolicyAdded models.EventType = "iam.policy.org.iam.added" OrgIAMPolicyChanged models.EventType = "iam.policy.org.iam.changed" ) diff --git a/internal/iam/repository/view/model/privacy_policy.go b/internal/iam/repository/view/model/privacy_policy.go new file mode 100644 index 0000000000..d66f0af276 --- /dev/null +++ b/internal/iam/repository/view/model/privacy_policy.go @@ -0,0 +1,98 @@ +package model + +import ( + "encoding/json" + "time" + + "github.com/caos/zitadel/internal/domain" + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + + es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + + "github.com/caos/logging" + + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/iam/model" +) + +const ( + PrivacyKeyAggregateID = "aggregate_id" +) + +type PrivacyPolicyView 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"` + State int32 `json:"-" gorm:"column:state"` + + TOSLink string `json:"tosLink" gorm:"column:tos_link"` + PrivacyLink string `json:"privacyLink" gorm:"column:privacy_link"` + Default bool `json:"-" gorm:"-"` + + Sequence uint64 `json:"-" gorm:"column:sequence"` +} + +func PrivacyViewFromModel(policy *model.PrivacyPolicyView) *PrivacyPolicyView { + return &PrivacyPolicyView{ + AggregateID: policy.AggregateID, + Sequence: policy.Sequence, + CreationDate: policy.CreationDate, + ChangeDate: policy.ChangeDate, + TOSLink: policy.TOSLink, + PrivacyLink: policy.PrivacyLink, + Default: policy.Default, + } +} + +func PrivacyViewToModel(policy *PrivacyPolicyView) *model.PrivacyPolicyView { + return &model.PrivacyPolicyView{ + AggregateID: policy.AggregateID, + Sequence: policy.Sequence, + CreationDate: policy.CreationDate, + ChangeDate: policy.ChangeDate, + TOSLink: policy.TOSLink, + PrivacyLink: policy.PrivacyLink, + Default: policy.Default, + } +} + +func (p *PrivacyPolicyView) ToDomain() *domain.PrivacyPolicy { + return &domain.PrivacyPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: p.AggregateID, + CreationDate: p.CreationDate, + ChangeDate: p.ChangeDate, + Sequence: p.Sequence, + }, + Default: p.Default, + TOSLink: p.TOSLink, + PrivacyLink: p.PrivacyLink, + } +} + +func (i *PrivacyPolicyView) AppendEvent(event *models.Event) (err error) { + i.Sequence = event.Sequence + i.ChangeDate = event.CreationDate + switch event.Type { + case es_model.PrivacyPolicyAdded, org_es_model.PrivacyPolicyAdded: + i.setRootData(event) + i.CreationDate = event.CreationDate + err = i.SetData(event) + case es_model.PrivacyPolicyChanged, org_es_model.PrivacyPolicyChanged: + err = i.SetData(event) + } + return err +} + +func (r *PrivacyPolicyView) setRootData(event *models.Event) { + r.AggregateID = event.AggregateID +} + +func (r *PrivacyPolicyView) SetData(event *models.Event) error { + if err := json.Unmarshal(event.Data, r); err != nil { + logging.Log("EVEN-gHls0").WithError(err).Error("could not unmarshal event data") + return caos_errs.ThrowInternal(err, "MODEL-Hs8uf", "Could not unmarshal data") + } + return nil +} diff --git a/internal/iam/repository/view/model/privacy_policy_query.go b/internal/iam/repository/view/model/privacy_policy_query.go new file mode 100644 index 0000000000..b8daf2e93b --- /dev/null +++ b/internal/iam/repository/view/model/privacy_policy_query.go @@ -0,0 +1,59 @@ +package model + +import ( + "github.com/caos/zitadel/internal/domain" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/view/repository" +) + +type PrivacyPolicySearchRequest iam_model.PrivacyPolicySearchRequest +type PrivacyPolicySearchQuery iam_model.PrivacyPolicySearchQuery +type PrivacyPolicySearchKey iam_model.PrivacyPolicySearchKey + +func (req PrivacyPolicySearchRequest) GetLimit() uint64 { + return req.Limit +} + +func (req PrivacyPolicySearchRequest) GetOffset() uint64 { + return req.Offset +} + +func (req PrivacyPolicySearchRequest) GetSortingColumn() repository.ColumnKey { + if req.SortingColumn == iam_model.PrivacyPolicySearchKeyUnspecified { + return nil + } + return PrivacyPolicySearchKey(req.SortingColumn) +} + +func (req PrivacyPolicySearchRequest) GetAsc() bool { + return req.Asc +} + +func (req PrivacyPolicySearchRequest) GetQueries() []repository.SearchQuery { + result := make([]repository.SearchQuery, len(req.Queries)) + for i, q := range req.Queries { + result[i] = PrivacyPolicySearchQuery{Key: q.Key, Value: q.Value, Method: q.Method} + } + return result +} + +func (req PrivacyPolicySearchQuery) GetKey() repository.ColumnKey { + return PrivacyPolicySearchKey(req.Key) +} + +func (req PrivacyPolicySearchQuery) GetMethod() domain.SearchMethod { + return req.Method +} + +func (req PrivacyPolicySearchQuery) GetValue() interface{} { + return req.Value +} + +func (key PrivacyPolicySearchKey) ToColumnName() string { + switch iam_model.PrivacyPolicySearchKey(key) { + case iam_model.PrivacyPolicySearchKeyAggregateID: + return PrivacyKeyAggregateID + default: + return "" + } +} diff --git a/internal/iam/repository/view/privacy_policy_view.go b/internal/iam/repository/view/privacy_policy_view.go new file mode 100644 index 0000000000..e53eea9ee6 --- /dev/null +++ b/internal/iam/repository/view/privacy_policy_view.go @@ -0,0 +1,32 @@ +package view + +import ( + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/view/repository" + "github.com/jinzhu/gorm" +) + +func GetPrivacyPolicyByAggregateID(db *gorm.DB, table, aggregateID string) (*model.PrivacyPolicyView, error) { + policy := new(model.PrivacyPolicyView) + aggregateIDQuery := &model.PrivacyPolicySearchQuery{Key: iam_model.PrivacyPolicySearchKeyAggregateID, Value: aggregateID, Method: domain.SearchMethodEquals} + query := repository.PrepareGetByQuery(table, aggregateIDQuery) + err := query(db, policy) + if caos_errs.IsNotFound(err) { + return nil, caos_errs.ThrowNotFound(nil, "VIEW-2N9fs", "Errors.IAM.PrivacyPolicy.NotExisting") + } + return policy, err +} + +func PutPrivacyPolicy(db *gorm.DB, table string, policy *model.PrivacyPolicyView) error { + save := repository.PrepareSave(table) + return save(db, policy) +} + +func DeletePrivacyPolicy(db *gorm.DB, table, aggregateID string) error { + delete := repository.PrepareDeleteByKey(table, model.PrivacyPolicySearchKey(iam_model.PrivacyPolicySearchKeyAggregateID), aggregateID) + + return delete(db) +} diff --git a/internal/management/repository/eventsourcing/eventstore/org.go b/internal/management/repository/eventsourcing/eventstore/org.go index fa65372211..a1eba7a215 100644 --- a/internal/management/repository/eventsourcing/eventstore/org.go +++ b/internal/management/repository/eventsourcing/eventstore/org.go @@ -526,6 +526,23 @@ func (repo *OrgRepository) GetDefaultPasswordLockoutPolicy(ctx context.Context) return iam_es_model.PasswordLockoutViewToModel(policy), nil } +func (repo *OrgRepository) GetPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) { + policy, err := repo.View.PrivacyPolicyByAggregateID(authz.GetCtxData(ctx).OrgID) + if errors.IsNotFound(err) { + return repo.GetDefaultPrivacyPolicy(ctx) + } + return iam_es_model.PrivacyViewToModel(policy), nil +} + +func (repo *OrgRepository) GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) { + policy, err := repo.View.PrivacyPolicyByAggregateID(repo.SystemDefaults.IamID) + if err != nil { + return nil, err + } + policy.Default = true + return iam_es_model.PrivacyViewToModel(policy), nil +} + func (repo *OrgRepository) GetDefaultMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) { template, err := repo.View.MailTemplateByAggregateID(repo.SystemDefaults.IamID) if err != nil { diff --git a/internal/management/repository/eventsourcing/handler/handler.go b/internal/management/repository/eventsourcing/handler/handler.go index 1610db3d36..8e7bc46168 100644 --- a/internal/management/repository/eventsourcing/handler/handler.go +++ b/internal/management/repository/eventsourcing/handler/handler.go @@ -81,6 +81,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es handler{view, bulkLimit, configs.cycleDuration("MessageText"), errorCount, es}), newFeatures( handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}), + newPrivacyPolicy( + handler{view, bulkLimit, configs.cycleDuration("PrivacyPolicy"), errorCount, es}), } } diff --git a/internal/management/repository/eventsourcing/handler/privacy_policy.go b/internal/management/repository/eventsourcing/handler/privacy_policy.go new file mode 100644 index 0000000000..39a8fd6bdb --- /dev/null +++ b/internal/management/repository/eventsourcing/handler/privacy_policy.go @@ -0,0 +1,106 @@ +package handler + +import ( + "github.com/caos/logging" + "github.com/caos/zitadel/internal/eventstore/v1" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + + 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" + iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" +) + +const ( + privacyPolicyTable = "management.privacy_policies" +) + +type PrivacyPolicy struct { + handler + subscription *v1.Subscription +} + +func newPrivacyPolicy(handler handler) *PrivacyPolicy { + h := &PrivacyPolicy{ + handler: handler, + } + + h.subscribe() + + return h +} + +func (p *PrivacyPolicy) subscribe() { + p.subscription = p.es.Subscribe(p.AggregateTypes()...) + go func() { + for event := range p.subscription.Events { + query.ReduceEvent(p, event) + } + }() +} + +func (p *PrivacyPolicy) ViewModel() string { + return privacyPolicyTable +} + +func (p *PrivacyPolicy) AggregateTypes() []es_models.AggregateType { + return []es_models.AggregateType{model.OrgAggregate, iam_es_model.IAMAggregate} +} + +func (p *PrivacyPolicy) CurrentSequence() (uint64, error) { + sequence, err := p.view.GetLatestPrivacyPolicySequence() + if err != nil { + return 0, err + } + return sequence.CurrentSequence, nil +} + +func (p *PrivacyPolicy) EventQuery() (*es_models.SearchQuery, error) { + sequence, err := p.view.GetLatestPrivacyPolicySequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(p.AggregateTypes()...). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (p *PrivacyPolicy) Reduce(event *es_models.Event) (err error) { + switch event.AggregateType { + case model.OrgAggregate, iam_es_model.IAMAggregate: + err = p.processPrivacyPolicy(event) + } + return err +} + +func (p *PrivacyPolicy) processPrivacyPolicy(event *es_models.Event) (err error) { + policy := new(iam_model.PrivacyPolicyView) + switch event.Type { + case iam_es_model.PrivacyPolicyAdded, model.PrivacyPolicyAdded: + err = policy.AppendEvent(event) + case iam_es_model.PrivacyPolicyChanged, model.PrivacyPolicyChanged: + policy, err = p.view.PrivacyPolicyByAggregateID(event.AggregateID) + if err != nil { + return err + } + err = policy.AppendEvent(event) + case model.PrivacyPolicyRemoved: + return p.view.DeletePrivacyPolicy(event.AggregateID, event) + default: + return p.view.ProcessedPrivacyPolicySequence(event) + } + if err != nil { + return err + } + return p.view.PutPrivacyPolicy(policy, event) +} + +func (p *PrivacyPolicy) OnError(event *es_models.Event, err error) error { + logging.LogWithFields("SPOOL-4N8sw", "id", event.AggregateID).WithError(err).Warn("something went wrong in privacy policy handler") + return spooler.HandleError(event, err, p.view.GetLatestPrivacyPolicyFailedEvent, p.view.ProcessedPrivacyPolicyFailedEvent, p.view.ProcessedPrivacyPolicySequence, p.errorCountUntilSkip) +} + +func (p *PrivacyPolicy) OnSuccess() error { + return spooler.HandleSuccess(p.view.UpdatePrivacyPolicySpoolerRunTimestamp) +} diff --git a/internal/management/repository/eventsourcing/view/privacy_policy.go b/internal/management/repository/eventsourcing/view/privacy_policy.go new file mode 100644 index 0000000000..5ca5d81707 --- /dev/null +++ b/internal/management/repository/eventsourcing/view/privacy_policy.go @@ -0,0 +1,53 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + privacyPolicyTable = "management.privacy_policies" +) + +func (v *View) PrivacyPolicyByAggregateID(aggregateID string) (*model.PrivacyPolicyView, error) { + return view.GetPrivacyPolicyByAggregateID(v.Db, privacyPolicyTable, aggregateID) +} + +func (v *View) PutPrivacyPolicy(policy *model.PrivacyPolicyView, event *models.Event) error { + err := view.PutPrivacyPolicy(v.Db, privacyPolicyTable, policy) + if err != nil { + return err + } + return v.ProcessedPrivacyPolicySequence(event) +} + +func (v *View) DeletePrivacyPolicy(aggregateID string, event *models.Event) error { + err := view.DeletePrivacyPolicy(v.Db, privacyPolicyTable, aggregateID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedPrivacyPolicySequence(event) +} + +func (v *View) GetLatestPrivacyPolicySequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(privacyPolicyTable) +} + +func (v *View) ProcessedPrivacyPolicySequence(event *models.Event) error { + return v.saveCurrentSequence(privacyPolicyTable, event) +} + +func (v *View) UpdatePrivacyPolicySpoolerRunTimestamp() error { + return v.updateSpoolerRunSequence(privacyPolicyTable) +} + +func (v *View) GetLatestPrivacyPolicyFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(privacyPolicyTable, sequence) +} + +func (v *View) ProcessedPrivacyPolicyFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/management/repository/org.go b/internal/management/repository/org.go index a0e3c5a22a..6dd1091b21 100644 --- a/internal/management/repository/org.go +++ b/internal/management/repository/org.go @@ -41,6 +41,9 @@ type OrgRepository interface { GetPasswordLockoutPolicy(ctx context.Context) (*iam_model.PasswordLockoutPolicyView, error) GetDefaultPasswordLockoutPolicy(ctx context.Context) (*iam_model.PasswordLockoutPolicyView, error) + GetPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) + GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) + GetDefaultMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) GetMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) diff --git a/internal/org/repository/eventsourcing/model/types.go b/internal/org/repository/eventsourcing/model/types.go index 17eda9952f..5863aa70bf 100644 --- a/internal/org/repository/eventsourcing/model/types.go +++ b/internal/org/repository/eventsourcing/model/types.go @@ -92,4 +92,8 @@ const ( PasswordLockoutPolicyAdded models.EventType = "org.policy.password.lockout.added" PasswordLockoutPolicyChanged models.EventType = "org.policy.password.lockout.changed" PasswordLockoutPolicyRemoved models.EventType = "org.policy.password.lockout.removed" + + PrivacyPolicyAdded models.EventType = "org.policy.privacy.added" + PrivacyPolicyChanged models.EventType = "org.policy.privacy.changed" + PrivacyPolicyRemoved models.EventType = "org.policy.privacy.removed" ) diff --git a/internal/repository/features/features.go b/internal/repository/features/features.go index d714209e0d..fa89f0ac75 100644 --- a/internal/repository/features/features.go +++ b/internal/repository/features/features.go @@ -36,6 +36,7 @@ type FeaturesSetEvent struct { LabelPolicyWatermark *bool `json:"labelPolicyWatermark,omitempty"` CustomDomain *bool `json:"customDomain,omitempty"` CustomText *bool `json:"customText,omitempty"` + PrivacyPolicy *bool `json:"privacyPolicy,omitempty"` } func (e *FeaturesSetEvent) Data() interface{} { @@ -159,6 +160,13 @@ func ChangeCustomText(customText bool) func(event *FeaturesSetEvent) { e.CustomText = &customText } } + +func ChangePrivacyPolicy(privacyPolicy bool) func(event *FeaturesSetEvent) { + return func(e *FeaturesSetEvent) { + e.PrivacyPolicy = &privacyPolicy + } +} + func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) { e := &FeaturesSetEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/internal/repository/iam/eventstore.go b/internal/repository/iam/eventstore.go index bcfc96f563..be1401a7e0 100644 --- a/internal/repository/iam/eventstore.go +++ b/internal/repository/iam/eventstore.go @@ -34,6 +34,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(PasswordComplexityPolicyChangedEventType, PasswordComplexityPolicyChangedEventMapper). RegisterFilterEventMapper(PasswordLockoutPolicyAddedEventType, PasswordLockoutPolicyAddedEventMapper). RegisterFilterEventMapper(PasswordLockoutPolicyChangedEventType, PasswordLockoutPolicyChangedEventMapper). + RegisterFilterEventMapper(PrivacyPolicyAddedEventType, PrivacyPolicyAddedEventMapper). + RegisterFilterEventMapper(PrivacyPolicyChangedEventType, PrivacyPolicyChangedEventMapper). RegisterFilterEventMapper(MemberAddedEventType, MemberAddedEventMapper). RegisterFilterEventMapper(MemberChangedEventType, MemberChangedEventMapper). RegisterFilterEventMapper(MemberRemovedEventType, MemberRemovedEventMapper). diff --git a/internal/repository/iam/policy_privacy.go b/internal/repository/iam/policy_privacy.go new file mode 100644 index 0000000000..1fecf393cb --- /dev/null +++ b/internal/repository/iam/policy_privacy.go @@ -0,0 +1,75 @@ +package iam + +import ( + "context" + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/policy" +) + +const ( + PrivacyPolicyAddedEventType = iamEventTypePrefix + policy.PrivacyPolicyAddedEventType + PrivacyPolicyChangedEventType = iamEventTypePrefix + policy.PrivacyPolicyChangedEventType +) + +type PrivacyPolicyAddedEvent struct { + policy.PrivacyPolicyAddedEvent +} + +func NewPrivacyPolicyAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + tosLink, + privacyLink string, +) *PrivacyPolicyAddedEvent { + return &PrivacyPolicyAddedEvent{ + PrivacyPolicyAddedEvent: *policy.NewPrivacyPolicyAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + PrivacyPolicyAddedEventType), + tosLink, + privacyLink), + } +} + +func PrivacyPolicyAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.PrivacyPolicyAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &PrivacyPolicyAddedEvent{PrivacyPolicyAddedEvent: *e.(*policy.PrivacyPolicyAddedEvent)}, nil +} + +type PrivacyPolicyChangedEvent struct { + policy.PrivacyPolicyChangedEvent +} + +func NewPrivacyPolicyChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []policy.PrivacyPolicyChanges, +) (*PrivacyPolicyChangedEvent, error) { + changedEvent, err := policy.NewPrivacyPolicyChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + PrivacyPolicyChangedEventType), + changes, + ) + if err != nil { + return nil, err + } + return &PrivacyPolicyChangedEvent{PrivacyPolicyChangedEvent: *changedEvent}, nil +} + +func PrivacyPolicyChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.PrivacyPolicyChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &PrivacyPolicyChangedEvent{PrivacyPolicyChangedEvent: *e.(*policy.PrivacyPolicyChangedEvent)}, nil +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 7602ad9e58..cc62e3187b 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -56,6 +56,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(PasswordLockoutPolicyAddedEventType, PasswordLockoutPolicyAddedEventMapper). RegisterFilterEventMapper(PasswordLockoutPolicyChangedEventType, PasswordLockoutPolicyChangedEventMapper). RegisterFilterEventMapper(PasswordLockoutPolicyRemovedEventType, PasswordLockoutPolicyRemovedEventMapper). + RegisterFilterEventMapper(PrivacyPolicyAddedEventType, PrivacyPolicyAddedEventMapper). + RegisterFilterEventMapper(PrivacyPolicyChangedEventType, PrivacyPolicyChangedEventMapper). + RegisterFilterEventMapper(PrivacyPolicyRemovedEventType, PrivacyPolicyRemovedEventMapper). RegisterFilterEventMapper(MailTemplateAddedEventType, MailTemplateAddedEventMapper). RegisterFilterEventMapper(MailTemplateChangedEventType, MailTemplateChangedEventMapper). RegisterFilterEventMapper(MailTemplateRemovedEventType, MailTemplateRemovedEventMapper). diff --git a/internal/repository/org/policy_privacy.go b/internal/repository/org/policy_privacy.go new file mode 100644 index 0000000000..133b44c07e --- /dev/null +++ b/internal/repository/org/policy_privacy.go @@ -0,0 +1,103 @@ +package org + +import ( + "context" + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/policy" +) + +var ( + PrivacyPolicyAddedEventType = orgEventTypePrefix + policy.PrivacyPolicyAddedEventType + PrivacyPolicyChangedEventType = orgEventTypePrefix + policy.PrivacyPolicyChangedEventType + PrivacyPolicyRemovedEventType = orgEventTypePrefix + policy.PrivacyPolicyRemovedEventType +) + +type PrivacyPolicyAddedEvent struct { + policy.PrivacyPolicyAddedEvent +} + +func NewPrivacyPolicyAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + tosLink, + privacyLink string, +) *PrivacyPolicyAddedEvent { + return &PrivacyPolicyAddedEvent{ + PrivacyPolicyAddedEvent: *policy.NewPrivacyPolicyAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + PrivacyPolicyAddedEventType), + tosLink, + privacyLink), + } +} + +func PrivacyPolicyAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.PrivacyPolicyAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &PrivacyPolicyAddedEvent{PrivacyPolicyAddedEvent: *e.(*policy.PrivacyPolicyAddedEvent)}, nil +} + +type PrivacyPolicyChangedEvent struct { + policy.PrivacyPolicyChangedEvent +} + +func NewPrivacyPolicyChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []policy.PrivacyPolicyChanges, +) (*PrivacyPolicyChangedEvent, error) { + changedEvent, err := policy.NewPrivacyPolicyChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + PrivacyPolicyChangedEventType), + changes, + ) + if err != nil { + return nil, err + } + return &PrivacyPolicyChangedEvent{PrivacyPolicyChangedEvent: *changedEvent}, nil +} + +func PrivacyPolicyChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.PrivacyPolicyChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &PrivacyPolicyChangedEvent{PrivacyPolicyChangedEvent: *e.(*policy.PrivacyPolicyChangedEvent)}, nil +} + +type PrivacyPolicyRemovedEvent struct { + policy.PrivacyPolicyRemovedEvent +} + +func NewPrivacyPolicyRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *PrivacyPolicyRemovedEvent { + return &PrivacyPolicyRemovedEvent{ + PrivacyPolicyRemovedEvent: *policy.NewPrivacyPolicyRemovedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + PrivacyPolicyRemovedEventType), + ), + } +} + +func PrivacyPolicyRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.PrivacyPolicyRemovedEventMapper(event) + if err != nil { + return nil, err + } + + return &PrivacyPolicyRemovedEvent{PrivacyPolicyRemovedEvent: *e.(*policy.PrivacyPolicyRemovedEvent)}, nil +} diff --git a/internal/repository/policy/policy_privacy.go b/internal/repository/policy/policy_privacy.go new file mode 100644 index 0000000000..32b3a65267 --- /dev/null +++ b/internal/repository/policy/policy_privacy.go @@ -0,0 +1,136 @@ +package policy + +import ( + "encoding/json" + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/repository" +) + +const ( + PrivacyPolicyAddedEventType = "policy.privacy.added" + PrivacyPolicyChangedEventType = "policy.privacy.changed" + PrivacyPolicyRemovedEventType = "policy.privacy.removed" +) + +type PrivacyPolicyAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + TOSLink string `json:"tosLink,omitempty"` + PrivacyLink string `json:"privacyLink,omitempty"` +} + +func (e *PrivacyPolicyAddedEvent) Data() interface{} { + return e +} + +func (e *PrivacyPolicyAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewPrivacyPolicyAddedEvent( + base *eventstore.BaseEvent, + tosLink, + privacyLink string, +) *PrivacyPolicyAddedEvent { + return &PrivacyPolicyAddedEvent{ + BaseEvent: *base, + TOSLink: tosLink, + PrivacyLink: privacyLink, + } +} + +func PrivacyPolicyAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e := &PrivacyPolicyAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "POLIC-2k0fs", "unable to unmarshal policy") + } + + return e, nil +} + +type PrivacyPolicyChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + TOSLink *string `json:"tosLink,omitempty"` + PrivacyLink *string `json:"privacyLink,omitempty"` +} + +func (e *PrivacyPolicyChangedEvent) Data() interface{} { + return e +} + +func (e *PrivacyPolicyChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewPrivacyPolicyChangedEvent( + base *eventstore.BaseEvent, + changes []PrivacyPolicyChanges, +) (*PrivacyPolicyChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "POLICY-PPo0s", "Errors.NoChangesFound") + } + changeEvent := &PrivacyPolicyChangedEvent{ + BaseEvent: *base, + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type PrivacyPolicyChanges func(*PrivacyPolicyChangedEvent) + +func ChangeTOSLink(tosLink string) func(*PrivacyPolicyChangedEvent) { + return func(e *PrivacyPolicyChangedEvent) { + e.TOSLink = &tosLink + } +} + +func ChangePrivacyLink(privacyLink string) func(*PrivacyPolicyChangedEvent) { + return func(e *PrivacyPolicyChangedEvent) { + e.PrivacyLink = &privacyLink + } +} + +func PrivacyPolicyChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e := &PrivacyPolicyChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "POLIC-22nf9", "unable to unmarshal policy") + } + + return e, nil +} + +type PrivacyPolicyRemovedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *PrivacyPolicyRemovedEvent) Data() interface{} { + return nil +} + +func (e *PrivacyPolicyRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewPrivacyPolicyRemovedEvent(base *eventstore.BaseEvent) *PrivacyPolicyRemovedEvent { + return &PrivacyPolicyRemovedEvent{ + BaseEvent: *base, + } +} + +func PrivacyPolicyRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + return &PrivacyPolicyRemovedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} diff --git a/internal/setup/config.go b/internal/setup/config.go index 8f8b44ab21..6a5d4df7c0 100644 --- a/internal/setup/config.go +++ b/internal/setup/config.go @@ -22,6 +22,7 @@ type IAMSetUp struct { Step14 *command.Step14 Step15 *command.Step15 Step16 *command.Step16 + Step17 *command.Step17 } func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) { @@ -44,6 +45,7 @@ func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) { setup.Step14, setup.Step15, setup.Step16, + setup.Step17, } { if step.Step() <= currentDone { continue diff --git a/internal/ui/login/handler/privacy_policy_handler.go b/internal/ui/login/handler/privacy_policy_handler.go new file mode 100644 index 0000000000..8f8289cc2e --- /dev/null +++ b/internal/ui/login/handler/privacy_policy_handler.go @@ -0,0 +1,15 @@ +package handler + +import ( + "net/http" + + iam_model "github.com/caos/zitadel/internal/iam/model" +) + +func (l *Login) getDefaultPrivacyPolicy(r *http.Request) (*iam_model.PrivacyPolicyView, error) { + policy, err := l.authRepo.GetDefaultPrivacyPolicy(r.Context()) + if err != nil { + return nil, err + } + return policy, nil +} diff --git a/internal/ui/login/handler/renderer.go b/internal/ui/login/handler/renderer.go index c26d652e23..b9411c9911 100644 --- a/internal/ui/login/handler/renderer.go +++ b/internal/ui/login/handler/renderer.go @@ -321,8 +321,19 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title baseData.LoginPolicy = authReq.LoginPolicy baseData.LabelPolicy = authReq.LabelPolicy baseData.IDPProviders = authReq.AllowedExternalIDPs + if authReq.PrivacyPolicy != nil { + baseData.TOSLink = authReq.PrivacyPolicy.TOSLink + baseData.PrivacyLink = authReq.PrivacyPolicy.PrivacyLink + } } else { - //TODO: How to handle LabelPolicy if no auth req (eg Register) + privacyPolicy, err := l.getDefaultPrivacyPolicy(r) + if err != nil { + return baseData + } + if privacyPolicy != nil { + baseData.TOSLink = privacyPolicy.TOSLink + baseData.PrivacyLink = privacyPolicy.PrivacyLink + } } return baseData } @@ -405,7 +416,6 @@ func (l *Login) isDisplayLoginNameSuffix(authReq *domain.AuthRequest) bool { } return authReq.LabelPolicy != nil && !authReq.LabelPolicy.HideLoginNameSuffix } - func getRequestID(authReq *domain.AuthRequest, r *http.Request) string { if authReq != nil { return authReq.ID @@ -437,6 +447,8 @@ type baseData struct { OrgName string PrimaryDomain string DisplayLoginNameSuffix bool + TOSLink string + PrivacyLink string AuthReqID string CSRF template.HTML Nonce string diff --git a/internal/ui/login/static/i18n/de.yaml b/internal/ui/login/static/i18n/de.yaml index de7bc4f23f..c4962b9d8a 100644 --- a/internal/ui/login/static/i18n/de.yaml +++ b/internal/ui/login/static/i18n/de.yaml @@ -160,9 +160,7 @@ Registration: TosConfirm: Ich akzeptiere die TosLinkText: AGBs TosConfirmAnd: und die - TosLink: https://docs.zitadel.ch/docs/legal/terms-of-service PrivacyLinkText: Datenschutzerklärung - PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy ExternalLogin: oder registriere dich mit einem externen Benutzer RegistrationOrg: @@ -187,9 +185,7 @@ RegistrationOrg: TosConfirm: Ich akzeptiere die TosLinkText: AGBs TosConfirmAnd: und die - TosLink: https://docs.zitadel.ch/docs/legal/terms-of-service PrivacyLinkText: Datenschutzerklärung - PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy LinkingUsersDone: Title: Benutzerlinking @@ -228,9 +224,7 @@ Actions: Footer: PoweredBy: Powered By Tos: AGB - TosLink: https://docs.zitadel.ch/docs/legal/terms-of-service Privacy: Datenschutzerklärung - PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy Help: Hilfe Errors: diff --git a/internal/ui/login/static/i18n/en.yaml b/internal/ui/login/static/i18n/en.yaml index 5197876fe9..ee8fc6be21 100644 --- a/internal/ui/login/static/i18n/en.yaml +++ b/internal/ui/login/static/i18n/en.yaml @@ -160,9 +160,7 @@ Registration: TosConfirm: I accept the TosLinkText: TOS TosConfirmAnd: and the - TosLink: https://docs.zitadel.ch/docs/legal/terms-of-service PrivacyLinkText: privacy policy - PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy ExternalLogin: or register with an external user RegistrationOrg: @@ -187,9 +185,7 @@ RegistrationOrg: TosConfirm: I accept the TosLinkText: TOS TosConfirmAnd: and the - TosLink: https://docs.zitadel.ch/docs/legal/terms-of-service PrivacyLinkText: privacy policy - PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy LoginSuccess: Title: Login successful @@ -228,9 +224,7 @@ Actions: Footer: PoweredBy: Powered By Tos: TOS - TosLink: https://docs.zitadel.ch/docs/legal/terms-of-service Privacy: Privacy policy - PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy Help: Help Errors: diff --git a/internal/ui/login/static/templates/footer.html b/internal/ui/login/static/templates/footer.html index 5cb89cc72a..8a08764156 100644 --- a/internal/ui/login/static/templates/footer.html +++ b/internal/ui/login/static/templates/footer.html @@ -1,3 +1,4 @@ + {{define "footer"}} {{end}} diff --git a/internal/ui/login/static/templates/register.html b/internal/ui/login/static/templates/register.html index 10c674c347..814fbf3e22 100644 --- a/internal/ui/login/static/templates/register.html +++ b/internal/ui/login/static/templates/register.html @@ -91,23 +91,31 @@ {{ .PasswordPolicyDescription }} + {{ if or .TOSLink .PrivacyLink }}