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
This commit is contained in:
Fabi 2021-07-05 10:36:51 +02:00 committed by GitHub
parent 91f1c88d4e
commit beb1c1604a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 3171 additions and 34 deletions

View File

@ -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
ButtonText: Login
Step17:
PrivacyPolicy:
TOSLink: https://docs.zitadel.ch/docs/legal/terms-of-service
PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ type FeaturesWriteModel struct {
LabelPolicyWatermark bool
CustomDomain bool
CustomText bool
PrivacyPolicy bool
}
func (wm *FeaturesWriteModel) Reduce() error {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,6 +47,7 @@ type AuthRequest struct {
LoginPolicy *LoginPolicy
AllowedExternalIDPs []*IDPProvider
LabelPolicy *LabelPolicy
PrivacyPolicy *PrivacyPolicy
}
type ExternalUser struct {

View File

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

View File

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

View File

@ -19,6 +19,7 @@ const (
Step14
Step15
Step16
Step17
//StepCount marks the the length of possible steps (StepCount-1 == last possible step)
StepCount
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:

View File

@ -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:

View File

@ -1,3 +1,4 @@
{{define "footer"}}
<footer>
{{ if hasWatermark .LabelPolicy }}
@ -7,8 +8,12 @@
</span>
{{end}}
<span class="fill-space"></span>
<a href="{{t "Footer.TosLink"}}" rel="noopener noreferrer" target="_blank" alt="TOS">{{t "Footer.Tos"}}</a>
<a href="{{t "Footer.PrivacyLink"}}" rel="noopener noreferrer" target="_blank" alt="Privacy Policy">{{t "Footer.Privacy"}}</a>
{{ if .TOSLink }}
<a href="{{.TOSLink}}" rel="noopener noreferrer" target="_blank" alt="TOS">{{t "Footer.Tos"}}</a>
{{ end }}
{{ if .PrivacyLink }}
<a href="{{.PrivacyLink}}" rel="noopener noreferrer" target="_blank" alt="Privacy Policy">{{t "Footer.Privacy"}}</a>
{{end}}
<a href="https://docs.zitadel.ch/docs/manuals/user-login" target="_black" alt="Help">{{t "Footer.Help"}}</a>
</footer>
{{end}}

View File

@ -91,23 +91,31 @@
{{ .PasswordPolicyDescription }}
</div>
{{ if or .TOSLink .PrivacyLink }}
<div class="lgn-field">
<label class="lgn-label">{{t "Registration.TosAndPrivacy"}}</label>
<div class="lgn-checkbox">
<input type="checkbox" id="register-term-confirmation"
name="register-term-confirmation" required>
<label for="register-term-confirmation">
{{t "Registration.TosConfirm"}}
<a class="tos-link" target="_blank" href="{{t "Registration.TosLink"}}" rel="noopener noreferrer">
{{t "Registration.TosLinkText"}}
</a>
{{t "Registration.TosConfirmAnd"}}
<a class="tos-link" target="_blank" href="{{t "Registration.PrivacyLink"}}" rel="noopener noreferrer">
{{t "Registration.PrivacyLinkText"}}
</a>
{{t "Registration.TosConfirm"}}
{{ if .TOSLink }}
<a class="tos-link" target="_blank" href="{{ .TOSLink }}" rel="noopener noreferrer">
{{t "Registration.TosLinkText"}}
</a>
{{end}}
{{ if and .TOSLink .PrivacyLink }}
{{t "Registration.TosConfirmAnd"}}
{{ end }}
{{ if .PrivacyLink }}
<a class="tos-link" target="_blank" href="{{ .PrivacyLink}}" rel="noopener noreferrer">
{{t "Registration.PrivacyLinkText"}}
</a>
{{end}}
</label>
</div>
</div>
{{ end }}
</div>
{{template "error-message" .}}

View File

@ -67,21 +67,32 @@
{{ .PasswordPolicyDescription }}
</div>
{{ if or .TOSLink .PrivacyLink }}
<div class="lgn-field">
<label class="lgn-label" for="register-term-confirmation">{{t "RegistrationOrg.TosAndPrivacy"}}</label>
<div class="lgn-checkbox">
<input class="lgn-input" type="checkbox" id="register-term-confirmation"
name="register-term-confirmation" required>
<label class="lgn-label" for="register-term-confirmation">
{{t "RegistrationOrg.TosConfirm"}}
<a class="tos-link" target="_blank" href="{{t "RegistrationOrg.TosLink"}}" rel="noopener noreferrer">{{t "RegistrationOrg.TosLinkText"}}</a>
{{t "Registration.TosConfirmAnd"}}
<a class="tos-link" target="_blank" href="{{t "Registration.PrivacyLink"}}" rel="noopener noreferrer">
{{t "Registration.PrivacyLinkText"}}
</a>
{{t "RegistrationOrg.TosConfirm"}}
{{ if .TOSLink }}
<a class="tos-link" target="_blank" href="{{.TOSLink}}" rel="noopener noreferrer">{{t "RegistrationOrg.TosLinkText"}}</a>
{{end}}
{{ if and .TOSLink .PrivacyLink }}
{{t "Registration.TosConfirmAnd"}}
{{end}}
{{ if .PrivacyLink }}
<a class="tos-link" target="_blank" href="{{.PrivacyLink}}" rel="noopener noreferrer">
{{t "Registration.PrivacyLinkText"}}
</a>
{{end}}
</label>
</div>
</div>
{{ end }}
</div>
{{template "error-message" .}}

View File

@ -0,0 +1,46 @@
ALTER TABLE adminapi.features ADD COLUMN privacy_policy BOOLEAN;
ALTER TABLE auth.features ADD COLUMN privacy_policy BOOLEAN;
ALTER TABLE authz.features ADD COLUMN privacy_policy BOOLEAN;
ALTER TABLE management.features ADD COLUMN privacy_policy BOOLEAN;
CREATE TABLE auth.privacy_policies (
aggregate_id TEXT,
creation_date TIMESTAMPTZ,
change_date TIMESTAMPTZ,
state SMALLINT,
sequence BIGINT,
tos_link STRING,
privacy_link STRING,
PRIMARY KEY (aggregate_id)
);
CREATE TABLE adminapi.privacy_policies (
aggregate_id TEXT,
creation_date TIMESTAMPTZ,
change_date TIMESTAMPTZ,
state SMALLINT,
sequence BIGINT,
tos_link STRING,
privacy_link STRING,
PRIMARY KEY (aggregate_id)
);
CREATE TABLE management.privacy_policies (
aggregate_id TEXT,
creation_date TIMESTAMPTZ,
change_date TIMESTAMPTZ,
state SMALLINT,
sequence BIGINT,
tos_link STRING,
privacy_link STRING,
PRIMARY KEY (aggregate_id)
);

View File

@ -1450,6 +1450,65 @@ service AdminService {
};
}
//Returns the privacy policy defined by the administrators of ZITADEL
rpc GetPrivacyPolicy(GetPrivacyPolicyRequest) returns (GetPrivacyPolicyResponse) {
option (google.api.http) = {
get: "/policies/privacy";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "policy";
tags: "privacy policy";
tags: "privacy";
responses: {
key: "200";
value: {
description: "default privacy policy";
};
};
};
}
//Updates the default privacy policy of ZITADEL
// it impacts all organisations without a customised policy
rpc UpdatePrivacyPolicy(UpdatePrivacyPolicyRequest) returns (UpdatePrivacyPolicyResponse) {
option (google.api.http) = {
put: "/policies/privacy";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "policy";
tags: "privacy policy";
tags: "privacy";
responses: {
key: "200";
value: {
description: "default privacy policy updated";
};
};
responses: {
key: "400";
value: {
description: "invalid argument";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
};
};
};
};
};
}
//Returns the custom text for initial message
rpc GetDefaultInitMessageText(GetDefaultInitMessageTextRequest) returns (GetDefaultInitMessageTextResponse) {
option (google.api.http) = {
@ -2397,6 +2456,7 @@ message SetDefaultFeaturesRequest {
bool label_policy_private_label = 15;
bool label_policy_watermark = 16;
bool custom_text = 17;
bool privacy_policy = 18;
}
message SetDefaultFeaturesResponse {
@ -2431,6 +2491,7 @@ message SetOrgFeaturesRequest {
bool label_policy_private_label = 16;
bool label_policy_watermark = 17;
bool custom_text = 18;
bool privacy_policy = 19;
}
message SetOrgFeaturesResponse {
@ -2891,6 +2952,22 @@ message UpdatePasswordLockoutPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message GetPrivacyPolicyRequest {}
message GetPrivacyPolicyResponse {
zitadel.policy.v1.PrivacyPolicy policy = 1;
}
message UpdatePrivacyPolicyRequest {
string tos_link = 1;
string privacy_link = 2;
}
message UpdatePrivacyPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultInitMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}

View File

@ -25,6 +25,7 @@ message Features {
bool label_policy_private_label = 14;
bool label_policy_watermark = 15;
bool custom_text = 16;
bool privacy_policy = 17;
}
message FeatureTier {

View File

@ -1927,6 +1927,70 @@ service ManagementService {
};
}
// Returns the privacy policy of the organisation
// With this policy privacy relevant things can be configured (e.g. tos link)
rpc GetPrivacyPolicy(GetPrivacyPolicyRequest) returns (GetPrivacyPolicyResponse) {
option (google.api.http) = {
get: "/policies/privacy"
};
option (zitadel.v1.auth_option) = {
permission: "policy.read"
};
}
// Returns the default privacy policy of the IAM
// With this policy the privacy relevant things can be configured (e.g tos link)
rpc GetDefaultPrivacyPolicy(GetDefaultPrivacyPolicyRequest) returns (GetDefaultPrivacyPolicyResponse) {
option (google.api.http) = {
get: "/policies/default/privacy"
};
option (zitadel.v1.auth_option) = {
permission: "policy.read"
};
}
// Add a custom privacy policy for the organisation
// With this policy privacy relevant things can be configured (e.g. tos link)
rpc AddCustomPrivacyPolicy(AddCustomPrivacyPolicyRequest) returns (AddCustomPrivacyPolicyResponse) {
option (google.api.http) = {
post: "/policies/privacy"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "privacy_policy"
};
}
// Update the privacy complexity policy for the organisation
// With this policy privacy relevant things can be configured (e.g. tos link)
rpc UpdateCustomPrivacyPolicy(UpdateCustomPrivacyPolicyRequest) returns (UpdateCustomPrivacyPolicyResponse) {
option (google.api.http) = {
put: "/policies/privacy"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "policy.write"
feature: "privacy_policy"
};
}
// Removes the privacy policy of the organisation
// The default policy of the IAM will trigger after
rpc ResetPrivacyPolicyToDefault(ResetPrivacyPolicyToDefaultRequest) returns (ResetPrivacyPolicyToDefaultResponse) {
option (google.api.http) = {
delete: "/policies/privacy"
};
option (zitadel.v1.auth_option) = {
permission: "policy.delete"
};
}
// Returns the active label policy of the organisation
// With this policy the private labeling can be configured (colors, etc.)
rpc GetLabelPolicy(GetLabelPolicyRequest) returns (GetLabelPolicyResponse) {
@ -3973,6 +4037,45 @@ message ResetPasswordLockoutPolicyToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message GetPrivacyPolicyRequest {}
message GetPrivacyPolicyResponse {
zitadel.policy.v1.PrivacyPolicy policy = 1;
}
//This is an empty request
message GetDefaultPrivacyPolicyRequest {}
message GetDefaultPrivacyPolicyResponse {
zitadel.policy.v1.PrivacyPolicy policy = 1;
}
message AddCustomPrivacyPolicyRequest {
string tos_link = 1;
string privacy_link = 2;
}
message AddCustomPrivacyPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
message UpdateCustomPrivacyPolicyRequest {
string tos_link = 1;
string privacy_link = 2;
}
message UpdateCustomPrivacyPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message ResetPrivacyPolicyToDefaultRequest {}
message ResetPrivacyPolicyToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message GetLabelPolicyRequest {}

View File

@ -220,4 +220,11 @@ message PasswordLockoutPolicy {
description: "defines if the organisation's admin changed the policy"
}
];
}
message PrivacyPolicy {
zitadel.v1.ObjectDetails details = 1;
string tos_link = 2;
string privacy_link = 3;
bool is_default = 4;
}