From 85a88256b960fea96f796b712aa1d34bf0c15e34 Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Thu, 16 May 2024 15:37:01 -0500 Subject: [PATCH] Add domain settings and smpt [ZITADOPER-1] --- ops/chart/crds/zitadelcluster-crd.yaml | 64 +++++++++++++++++++ src/api/v1alpha1/zitadelcluster_types.go | 30 ++++++++- src/api/v1alpha1/zz_generated.deepcopy.go | 58 +++++++++++++++++ ...zitadel.topmanage.com_zitadelclusters.yaml | 64 +++++++++++++++++++ .../controller/zitadelcluster_controller.go | 52 +++++++++++++++ src/pkg/zitadel/zitadel.go | 26 ++++++-- 6 files changed, 287 insertions(+), 7 deletions(-) diff --git a/ops/chart/crds/zitadelcluster-crd.yaml b/ops/chart/crds/zitadelcluster-crd.yaml index 9d1c002..6ab104b 100644 --- a/ops/chart/crds/zitadelcluster-crd.yaml +++ b/ops/chart/crds/zitadelcluster-crd.yaml @@ -70,6 +70,22 @@ spec: type: string type: object x-kubernetes-map-type: atomic + domainSettings: + properties: + smtpSenderAddressMatchesInstanceDomain: + default: true + type: boolean + userLoginMustBeDomain: + default: true + type: boolean + validateOrgDomains: + default: true + type: boolean + required: + - smtpSenderAddressMatchesInstanceDomain + - userLoginMustBeDomain + - validateOrgDomains + type: object externalPort: default: 443 format: int64 @@ -164,8 +180,55 @@ spec: type: string description: ServiceAnnotations to add to the service metadata. type: object + smtpConfig: + properties: + host: + type: string + password: + properties: + secretRef: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object + replyToAddress: + type: string + senderAddress: + type: string + senderName: + type: string + tls: + default: true + type: boolean + user: + type: string + required: + - host + - replyToAddress + - senderAddress + - senderName + - tls + type: object required: - crdbClusterRef + - domainSettings - externalPort - externalSecure - firstOrgName @@ -173,6 +236,7 @@ spec: - image - purpose - resources + - smtpConfig type: object status: description: ZitadelClusterStatus defines the observed state of ZitadelCluster diff --git a/src/api/v1alpha1/zitadelcluster_types.go b/src/api/v1alpha1/zitadelcluster_types.go index 8d607fd..133452c 100644 --- a/src/api/v1alpha1/zitadelcluster_types.go +++ b/src/api/v1alpha1/zitadelcluster_types.go @@ -30,13 +30,39 @@ type Image struct { Tag string `json:"tag"` } +type Password struct { + SecretKeyRef corev1.SecretKeySelector `json:"secretRef"` +} + +type SMTPConfig struct { + SenderAddress string `json:"senderAddress"` + SenderName string `json:"senderName"` + // +kubebuilder:default=true + TLS bool `json:"tls"` + Host string `json:"host"` + User *string `json:"user,omitempty"` + Password *Password `json:"password,omitempty"` + ReplyToAddress string `json:"replyToAddress"` +} + +type DomainSettings struct { + // +kubebuilder:default=true + UserLoginMustBeDomain bool `json:"userLoginMustBeDomain"` + // +kubebuilder:default=true + ValidateOrgDomains bool `json:"validateOrgDomains"` + // +kubebuilder:default=true + SMTPSenderAddressMatchesInstanceDomain bool `json:"smtpSenderAddressMatchesInstanceDomain"` +} + // ZitadelClusterSpec defines the desired state of ZitadelCluster type ZitadelClusterSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // +kubebuilder:default="DEFAULT" - FirstOrgName string `json:"firstOrgName"` - Host string `json:"host"` + FirstOrgName string `json:"firstOrgName"` + DomainSettings DomainSettings `json:"domainSettings"` + SMTPConfig SMTPConfig `json:"smtpConfig"` + Host string `json:"host"` // +kubebuilder:default=443 ExternalPort int64 `json:"externalPort"` // +kubebuilder:default=true diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index c77ccfe..0a21d5d 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -139,6 +139,21 @@ func (in *CrdbClusterRef) DeepCopy() *CrdbClusterRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DomainSettings) DeepCopyInto(out *DomainSettings) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DomainSettings. +func (in *DomainSettings) DeepCopy() *DomainSettings { + if in == nil { + return nil + } + out := new(DomainSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Image) DeepCopyInto(out *Image) { *out = *in @@ -491,6 +506,22 @@ func (in *OrganizationStatus) DeepCopy() *OrganizationStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Password) DeepCopyInto(out *Password) { + *out = *in + in.SecretKeyRef.DeepCopyInto(&out.SecretKeyRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Password. +func (in *Password) DeepCopy() *Password { + if in == nil { + return nil + } + out := new(Password) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Project) DeepCopyInto(out *Project) { *out = *in @@ -624,6 +655,31 @@ func (in *Role) DeepCopy() *Role { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SMTPConfig) DeepCopyInto(out *SMTPConfig) { + *out = *in + if in.User != nil { + in, out := &in.User, &out.User + *out = new(string) + **out = **in + } + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(Password) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SMTPConfig. +func (in *SMTPConfig) DeepCopy() *SMTPConfig { + if in == nil { + return nil + } + out := new(SMTPConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ZitadelCluster) DeepCopyInto(out *ZitadelCluster) { *out = *in @@ -702,6 +758,8 @@ func (in *ZitadelClusterRef) DeepCopy() *ZitadelClusterRef { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ZitadelClusterSpec) DeepCopyInto(out *ZitadelClusterSpec) { *out = *in + out.DomainSettings = in.DomainSettings + in.SMTPConfig.DeepCopyInto(&out.SMTPConfig) out.Image = in.Image in.Resources.DeepCopyInto(&out.Resources) out.CrdbClusterRef = in.CrdbClusterRef diff --git a/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml b/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml index ef10f9a..01541e4 100644 --- a/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml +++ b/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml @@ -71,6 +71,22 @@ spec: type: string type: object x-kubernetes-map-type: atomic + domainSettings: + properties: + smtpSenderAddressMatchesInstanceDomain: + default: true + type: boolean + userLoginMustBeDomain: + default: true + type: boolean + validateOrgDomains: + default: true + type: boolean + required: + - smtpSenderAddressMatchesInstanceDomain + - userLoginMustBeDomain + - validateOrgDomains + type: object externalPort: default: 443 format: int64 @@ -165,8 +181,55 @@ spec: type: string description: ServiceAnnotations to add to the service metadata. type: object + smtpConfig: + properties: + host: + type: string + password: + properties: + secretRef: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object + replyToAddress: + type: string + senderAddress: + type: string + senderName: + type: string + tls: + default: true + type: boolean + user: + type: string + required: + - host + - replyToAddress + - senderAddress + - senderName + - tls + type: object required: - crdbClusterRef + - domainSettings - externalPort - externalSecure - firstOrgName @@ -174,6 +237,7 @@ spec: - image - purpose - resources + - smtpConfig type: object status: description: ZitadelClusterStatus defines the observed state of ZitadelCluster diff --git a/src/internal/controller/zitadelcluster_controller.go b/src/internal/controller/zitadelcluster_controller.go index 2813122..006ff2c 100644 --- a/src/internal/controller/zitadelcluster_controller.go +++ b/src/internal/controller/zitadelcluster_controller.go @@ -42,6 +42,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" "github.com/zitadel/zitadel-go/v2/pkg/client/system" + adm "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/admin" authn "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/authn" "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/system" @@ -152,6 +153,14 @@ func (r *ZitadelClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque Name: "DefaultInstance", Reconcile: r.reconcileDefaultInstance, }, + { + Name: "SMTPConfig", + Reconcile: r.reconcileSMTPConfig, + }, + { + Name: "DomainPolicyConfig", + Reconcile: r.reconcileDomainPolicy, + }, { Name: "InitialAdminSecret", Reconcile: r.reconcileInitialAdminPassword, @@ -448,6 +457,48 @@ func (r *ZitadelClusterReconciler) reconcileDefaultInstance(ctx context.Context, return ctrl.Result{}, nil } +func (r *ZitadelClusterReconciler) reconcileSMTPConfig(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { + adminClient, err := zitadelClient.NewAdminClient(ctx, zitadel, *r.RefResolver) + if err != nil { + return ctrl.Result{}, err + } + adminRequest := &adm.AddSMTPConfigRequest{ + SenderAddress: zitadel.Spec.SMTPConfig.SenderAddress, + SenderName: zitadel.Spec.SMTPConfig.SenderName, + Tls: zitadel.Spec.SMTPConfig.TLS, + Host: zitadel.Spec.SMTPConfig.Host, + ReplyToAddress: zitadel.Spec.SMTPConfig.ReplyToAddress, + } + if zitadel.Spec.SMTPConfig.User != nil && zitadel.Spec.SMTPConfig.Password != nil { + passwordSecret, err := r.RefResolver.SecretKeyRef(ctx, zitadel.Spec.SMTPConfig.Password.SecretKeyRef, zitadel.Namespace) + if err != nil { + return ctrl.Result{}, err + } + adminRequest.Password = passwordSecret + adminRequest.User = *zitadel.Spec.SMTPConfig.User + } + + if _, err = adminClient.AddSMTPConfig(ctx, adminRequest); err != nil { + return ctrl.Result{}, fmt.Errorf("Could not add SMTP config: %v", err) + } + return ctrl.Result{}, nil +} + +func (r *ZitadelClusterReconciler) reconcileDomainPolicy(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { + adminClient, err := zitadelClient.NewAdminClient(ctx, zitadel, *r.RefResolver) + if err != nil { + return ctrl.Result{}, err + } + if _, err = adminClient.UpdateDomainPolicy(ctx, &adm.UpdateDomainPolicyRequest{ + UserLoginMustBeDomain: zitadel.Spec.DomainSettings.UserLoginMustBeDomain, + ValidateOrgDomains: zitadel.Spec.DomainSettings.ValidateOrgDomains, + SmtpSenderAddressMatchesInstanceDomain: zitadel.Spec.DomainSettings.SMTPSenderAddressMatchesInstanceDomain, + }); err != nil { + return ctrl.Result{}, fmt.Errorf("Could not update domain policy config: %v", err) + } + return ctrl.Result{}, nil +} + func (r *ZitadelClusterReconciler) reconcileInitialAdminPassword(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { secretName := admin.AdminPasswordSecretName(zitadel) key := types.NamespacedName{ @@ -540,6 +591,7 @@ func (r *ZitadelClusterReconciler) reconcileInitialHumanUser(ctx context.Context zitadel.Status.InitialAdminId = userid return ctrl.Result{}, r.Status().Patch(ctx, zitadel, patch) } + func GetIssuer(zitadel *zitadelv1alpha1.ZitadelCluster) string { scheme := "http" if zitadel.Spec.ExternalSecure { diff --git a/src/pkg/zitadel/zitadel.go b/src/pkg/zitadel/zitadel.go index c52e347..6acfc6c 100644 --- a/src/pkg/zitadel/zitadel.go +++ b/src/pkg/zitadel/zitadel.go @@ -1,22 +1,24 @@ package zitadel import ( - zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" - "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/deployment" - "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/machinekey" "context" "encoding/json" "fmt" - "github.com/gorilla/schema" - "google.golang.org/grpc" "net/http" "reflect" "strings" "time" + zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" + "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/deployment" + "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/machinekey" + "github.com/gorilla/schema" + "google.golang.org/grpc" + "github.com/zitadel/oidc/pkg/client" httphelper "github.com/zitadel/oidc/pkg/http" "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/zitadel-go/v2/pkg/client/admin" "github.com/zitadel/zitadel-go/v2/pkg/client/management" "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel" "golang.org/x/oauth2" @@ -46,6 +48,20 @@ func NewClient(ctx context.Context, zitadelCluster *zitadelv1alpha1.ZitadelClust return api, nil } +func NewAdminClient(ctx context.Context, zitadelCluster *zitadelv1alpha1.ZitadelCluster, refresolver zitadelv1alpha1.RefResolver) (*admin.Client, error) { + machineKeyData, err := refresolver.SecretKeyRef(ctx, corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: machinekey.MachineKeySecretName(zitadelCluster)}, Key: machinekey.Key}, zitadelCluster.Namespace) + if err != nil { + return nil, err + } + api, err := admin.NewClient(GetIssuer(zitadelCluster), GetAPI(zitadelCluster), []string{oidc.ScopeOpenID, zitadel.ScopeZitadelAPI()}, zitadel.WithInsecure(), zitadel.WithJWTProfileTokenSource(Discover([]byte(machineKeyData), GetAPIUrl(zitadelCluster), GetAuthority(zitadelCluster), GetAPI(zitadelCluster))), + zitadel.WithDialOptions(grpc.WithAuthority(GetAuthority(zitadelCluster))), + ) + if err != nil { + return nil, fmt.Errorf("ERROR CREATING CLIENT: %v", err) + } + return api, nil +} + func GetAuthority(zitadel *zitadelv1alpha1.ZitadelCluster) string { return fmt.Sprintf("%s:%d", zitadel.Spec.Host, zitadel.Spec.ExternalPort) }