From 624b99d371399f87838328d423a558929db2922d Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Wed, 15 May 2024 19:49:16 -0500 Subject: [PATCH] Add initial admin [ZITADOPER-1] --- ops/chart/crds/machineuser-crd.yaml | 4 +- ops/chart/crds/zitadelcluster-crd.yaml | 10 +- src/api/v1alpha1/apiapp_types.go | 8 ++ src/api/v1alpha1/machineuser_types.go | 18 +++- src/api/v1alpha1/oidcapp_types.go | 10 ++ src/api/v1alpha1/project_types.go | 5 + src/api/v1alpha1/zitadelcluster_types.go | 6 +- src/api/v1alpha1/zz_generated.deepcopy.go | 2 +- .../zitadel.topmanage.com_machineusers.yaml | 4 +- ...zitadel.topmanage.com_zitadelclusters.yaml | 10 +- .../controller/machineuser_controller.go | 39 +++++--- src/internal/controller/oidcapp_controller.go | 1 + .../controller/organization_controller.go | 5 +- .../controller/zitadelcluster_controller.go | 99 ++++++++++++++++++- 14 files changed, 197 insertions(+), 24 deletions(-) diff --git a/ops/chart/crds/machineuser-crd.yaml b/ops/chart/crds/machineuser-crd.yaml index f32e665..12327c2 100644 --- a/ops/chart/crds/machineuser-crd.yaml +++ b/ops/chart/crds/machineuser-crd.yaml @@ -39,7 +39,7 @@ spec: - ACCESS_TOKEN_TYPE_BEARER - ACCESS_TOKEN_TYPE_JWT type: string - zitadelClusterRef: + organizationRef: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' properties: @@ -79,7 +79,7 @@ spec: x-kubernetes-map-type: atomic required: - accessTokenType - - zitadelClusterRef + - organizationRef type: object status: description: MachineUserStatus defines the observed state of MachineUser diff --git a/ops/chart/crds/zitadelcluster-crd.yaml b/ops/chart/crds/zitadelcluster-crd.yaml index 729837f..9d1c002 100644 --- a/ops/chart/crds/zitadelcluster-crd.yaml +++ b/ops/chart/crds/zitadelcluster-crd.yaml @@ -77,10 +77,13 @@ spec: externalSecure: default: true type: boolean - host: + firstOrgName: + default: DEFAULT description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' type: string + host: + type: string image: properties: name: @@ -165,6 +168,7 @@ spec: - crdbClusterRef - externalPort - externalSecure + - firstOrgName - host - image - purpose @@ -247,12 +251,16 @@ spec: defaultInstanceId: default: "" type: string + initialAdminId: + default: "" + type: string replicas: default: 3 format: int32 type: integer required: - defaultInstanceId + - initialAdminId type: object type: object served: true diff --git a/src/api/v1alpha1/apiapp_types.go b/src/api/v1alpha1/apiapp_types.go index 1364e22..7b7641d 100644 --- a/src/api/v1alpha1/apiapp_types.go +++ b/src/api/v1alpha1/apiapp_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "context" + "fmt" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -82,10 +83,17 @@ func (d *APIApp) ZitadelClusterRef(ctx context.Context, refresolver *RefResolver if err != nil { return nil, err } + if project.Status.ProjectId == "" { + return nil, fmt.Errorf("Project has not been created yet...") + } org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) if err != nil { return nil, err } + + if org.Status.OrgId == "" { + return nil, fmt.Errorf("Organization has not been created yet...") + } ref, err := org.ZitadelClusterRef(ctx, refresolver) if err != nil { return nil, err diff --git a/src/api/v1alpha1/machineuser_types.go b/src/api/v1alpha1/machineuser_types.go index f057ac9..da1d743 100644 --- a/src/api/v1alpha1/machineuser_types.go +++ b/src/api/v1alpha1/machineuser_types.go @@ -18,6 +18,8 @@ package v1alpha1 import ( "context" + "fmt" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -31,7 +33,7 @@ type MachineUserSpec struct { // Important: Run "make" to regenerate code after modifying this file // +kubebuilder:validation:Required // +operator-sdk:csv:customresourcedefinitions:type=spec - ZitadelClusterRef ZitadelClusterRef `json:"zitadelClusterRef" webhook:"inmutable"` + OrganizationRef OrganizationRef `json:"organizationRef" webhook:"inmutable"` // +kubebuilder:validation:Enum=ACCESS_TOKEN_TYPE_BEARER;ACCESS_TOKEN_TYPE_JWT AccessTokenType string `json:"accessTokenType"` } @@ -77,7 +79,19 @@ func (d *MachineUser) IsReady() bool { } func (d *MachineUser) ZitadelClusterRef(ctx context.Context, refresolver *RefResolver) (*ZitadelClusterRef, error) { - return &d.Spec.ZitadelClusterRef, nil + org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + + if org.Status.OrgId == "" { + return nil, fmt.Errorf("Organization has not been created yet...") + } + ref, err := org.ZitadelClusterRef(ctx, refresolver) + if err != nil { + return nil, err + } + return ref, nil } //+kubebuilder:object:root=true diff --git a/src/api/v1alpha1/oidcapp_types.go b/src/api/v1alpha1/oidcapp_types.go index 9c2a146..21db63b 100644 --- a/src/api/v1alpha1/oidcapp_types.go +++ b/src/api/v1alpha1/oidcapp_types.go @@ -18,6 +18,8 @@ package v1alpha1 import ( "context" + "fmt" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -103,10 +105,18 @@ func (d *OIDCApp) ZitadelClusterRef(ctx context.Context, refresolver *RefResolve if err != nil { return nil, err } + + if project.Status.ProjectId == "" { + return nil, fmt.Errorf("Project has not been created yet...") + } org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) if err != nil { return nil, err } + + if org.Status.OrgId == "" { + return nil, fmt.Errorf("Organization has not been created yet...") + } ref, err := org.ZitadelClusterRef(ctx, refresolver) if err != nil { return nil, err diff --git a/src/api/v1alpha1/project_types.go b/src/api/v1alpha1/project_types.go index 0724451..8926db4 100644 --- a/src/api/v1alpha1/project_types.go +++ b/src/api/v1alpha1/project_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "context" + "fmt" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -94,6 +95,10 @@ func (d *Project) ZitadelClusterRef(ctx context.Context, refresolver *RefResolve if err != nil { return nil, err } + if org.Status.OrgId == "" { + return nil, fmt.Errorf("Organization has not been created yet...") + } + ref, err := org.ZitadelClusterRef(ctx, refresolver) if err != nil { return nil, err diff --git a/src/api/v1alpha1/zitadelcluster_types.go b/src/api/v1alpha1/zitadelcluster_types.go index 655e55a..8d607fd 100644 --- a/src/api/v1alpha1/zitadelcluster_types.go +++ b/src/api/v1alpha1/zitadelcluster_types.go @@ -34,7 +34,9 @@ type Image struct { type ZitadelClusterSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - Host string `json:"host"` + // +kubebuilder:default="DEFAULT" + FirstOrgName string `json:"firstOrgName"` + Host string `json:"host"` // +kubebuilder:default=443 ExternalPort int64 `json:"externalPort"` // +kubebuilder:default=true @@ -67,6 +69,8 @@ type ZitadelClusterStatus struct { Replicas int32 `json:"replicas,omitempty"` // +kubebuilder:default="" DefaultInstanceId string `json:"defaultInstanceId"` + // +kubebuilder:default="" + InitialAdminId string `json:"initialAdminId"` } // SetCondition sets a status condition diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index 86f3a0f..c77ccfe 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -216,7 +216,7 @@ func (in *MachineUserList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MachineUserSpec) DeepCopyInto(out *MachineUserSpec) { *out = *in - out.ZitadelClusterRef = in.ZitadelClusterRef + out.OrganizationRef = in.OrganizationRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUserSpec. diff --git a/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml b/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml index 27fbee0..a4fbc88 100644 --- a/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml +++ b/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml @@ -40,7 +40,7 @@ spec: - ACCESS_TOKEN_TYPE_BEARER - ACCESS_TOKEN_TYPE_JWT type: string - zitadelClusterRef: + organizationRef: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' properties: @@ -80,7 +80,7 @@ spec: x-kubernetes-map-type: atomic required: - accessTokenType - - zitadelClusterRef + - organizationRef type: object status: description: MachineUserStatus defines the observed state of MachineUser diff --git a/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml b/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml index 720233c..ef10f9a 100644 --- a/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml +++ b/src/config/crd/bases/zitadel.topmanage.com_zitadelclusters.yaml @@ -78,10 +78,13 @@ spec: externalSecure: default: true type: boolean - host: + firstOrgName: + default: DEFAULT description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' type: string + host: + type: string image: properties: name: @@ -166,6 +169,7 @@ spec: - crdbClusterRef - externalPort - externalSecure + - firstOrgName - host - image - purpose @@ -248,12 +252,16 @@ spec: defaultInstanceId: default: "" type: string + initialAdminId: + default: "" + type: string replicas: default: 3 format: int32 type: integer required: - defaultInstanceId + - initialAdminId type: object type: object served: true diff --git a/src/internal/controller/machineuser_controller.go b/src/internal/controller/machineuser_controller.go index 1f598d7..2130b07 100644 --- a/src/internal/controller/machineuser_controller.go +++ b/src/internal/controller/machineuser_controller.go @@ -11,6 +11,7 @@ import ( condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel" "github.com/zitadel/zitadel-go/v2/pkg/client/management" + "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/authn" pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" user "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/user" @@ -85,12 +86,27 @@ func newWrappedMachineUserReconciler(client client.Client, refResolver *zitadelv func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { // TODO: update machine user - zitadel, err := wr.refResolver.ZitadelCluster(ctx, &wr.MachineUser.Spec.ZitadelClusterRef, wr.MachineUser.Namespace) + org, err := wr.refResolver.OrganizationRef(ctx, &wr.MachineUser.Spec.OrganizationRef, wr.MachineUser.Namespace) if err != nil { return err } + if wr.MachineUser.Status.UserId != "" { + _, err = ztdClient.UpdateMachine(middleware.SetOrgID(ctx, org.Status.OrgId), + &pb.UpdateMachineRequest{ + UserId: wr.MachineUser.Status.UserId, + Name: wr.MachineUser.Name, + Description: wr.MachineUser.Name, + AccessTokenType: user.AccessTokenType(user.AccessTokenType_value[wr.MachineUser.Spec.AccessTokenType]), + }) + if err != nil { + if !strings.Contains(err.Error(), "No changes") { + return fmt.Errorf("Error updating OIDCApp: %v", err) + } + } + return nil + } - resp, err := ztdClient.AddMachineUser(ctx, + resp, err := ztdClient.AddMachineUser(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.AddMachineUserRequest{ Name: wr.MachineUser.Name, UserName: wr.MachineUser.Name, @@ -121,17 +137,18 @@ func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient return fmt.Errorf("Error Adding MachineKey: %v", err) } key := types.NamespacedName{ - Name: wr.MachineUser.Name + "-key-secret", + Name: wr.MachineUser.Name + "-machinekey-secret", Namespace: wr.MachineUser.Namespace, } - secret, err := wr.Builder.BuildSecret(builder.SecretOpts{ - Zitadel: zitadel, - Key: key, - Immutable: true, - Data: map[string][]byte{ - "key": respKey.KeyDetails, - }, - }, wr.MachineUser) + secret, err := wr.Builder.BuildSecret( + builder.SecretOpts{ + Key: key, + Immutable: true, + Data: map[string][]byte{ + "key.json": respKey.KeyDetails, + }, + }, wr.MachineUser) + if err != nil { return fmt.Errorf("error building Secret: %v", err) } diff --git a/src/internal/controller/oidcapp_controller.go b/src/internal/controller/oidcapp_controller.go index 2b8a693..f029a14 100644 --- a/src/internal/controller/oidcapp_controller.go +++ b/src/internal/controller/oidcapp_controller.go @@ -122,6 +122,7 @@ func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *ma ProjectId: project.Status.ProjectId, AppId: string(wr.OIDCApp.Status.AppId), }) + // TODO: fix flow if err != nil { return fmt.Errorf("Error getting OIDCApp: %v", err) } diff --git a/src/internal/controller/organization_controller.go b/src/internal/controller/organization_controller.go index d7b0c79..8767d4f 100644 --- a/src/internal/controller/organization_controller.go +++ b/src/internal/controller/organization_controller.go @@ -91,7 +91,10 @@ func newWrappedOrganizationReconciler(client client.Client, refResolver *zitadel } func (wr *wrappedOrganizationReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { - resp, err := ztdClient.AddOrg(ctx, &pb.AddOrgRequest{Name: wr.organization.Name}) + // TODO: check if org exists first + resp, err := ztdClient.AddOrg(ctx, &pb.AddOrgRequest{ + Name: wr.organization.Name, + }) if err != nil { if strings.Contains(err.Error(), "AlreadyExists") { return nil diff --git a/src/internal/controller/zitadelcluster_controller.go b/src/internal/controller/zitadelcluster_controller.go index 1edf031..742505a 100644 --- a/src/internal/controller/zitadelcluster_controller.go +++ b/src/internal/controller/zitadelcluster_controller.go @@ -27,6 +27,7 @@ import ( "time" zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" + "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/admin" builder "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/builder" condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/configuration" @@ -39,9 +40,12 @@ import ( systemapiaccount "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/systemapi" zitadelClient "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/zitadel" "github.com/hashicorp/go-multierror" + "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" "github.com/zitadel/zitadel-go/v2/pkg/client/system" 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" + "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/user" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -148,6 +152,14 @@ func (r *ZitadelClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque Name: "DefaultInstance", Reconcile: r.reconcileDefaultInstance, }, + { + Name: "InitialAdminSecret", + Reconcile: r.reconcileInitialAdminPassword, + }, + { + Name: "InitialAdmin", + Reconcile: r.reconcileInitialHumanUser, + }, } for _, p := range phases { @@ -390,8 +402,8 @@ func (r *ZitadelClusterReconciler) reconcileDefaultInstance(ctx context.Context, if strings.Contains(err.Error(), "Instance not found") { // if Instance doesn't exist, then create and assign secrets resp, err := ztdClient.CreateInstance(ctx, &pb.CreateInstanceRequest{ - InstanceName: "DEFAULT", - FirstOrgName: "DEFAULT", + InstanceName: zitadel.Spec.FirstOrgName, + FirstOrgName: zitadel.Spec.FirstOrgName, CustomDomain: zitadel.Spec.Host, Owner: &pb.CreateInstanceRequest_Machine_{Machine: &pb.CreateInstanceRequest_Machine{ Name: "k8s-operator", @@ -436,6 +448,89 @@ func (r *ZitadelClusterReconciler) reconcileDefaultInstance(ctx context.Context, return ctrl.Result{}, nil } +func (r *ZitadelClusterReconciler) reconcileInitialAdminPassword(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { + secretName := admin.AdminPasswordSecretName(zitadel) + key := types.NamespacedName{ + Name: secretName, + Namespace: zitadel.Namespace, + } + _, err := r.SecretReconciler.ReconcileRandomPassword(ctx, key, systemapiaccount.Key, zitadel) + + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +func (r *ZitadelClusterReconciler) reconcileInitialHumanUser(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { + managementClient, err := zitadelClient.NewClient(ctx, zitadel, *r.RefResolver) + if err != nil { + return ctrl.Result{}, err + } + defer managementClient.Connection.Close() + + secretName := admin.AdminPasswordSecretName(zitadel) + key := types.NamespacedName{ + Name: secretName, + Namespace: zitadel.Namespace, + } + password, err := r.SecretReconciler.ReconcileRandomPassword(ctx, key, admin.Key, zitadel) + if err != nil { + return ctrl.Result{}, err + } + + org, err := managementClient.GetMyOrg(ctx, &management.GetMyOrgRequest{}) + if err != nil { + return ctrl.Result{}, fmt.Errorf("Error getting org: %v", err) + } + users, err := managementClient.ListUsers(middleware.SetOrgID(ctx, org.Org.Id), &management.ListUsersRequest{}) + if err != nil { + return ctrl.Result{}, fmt.Errorf("Error getting users: %v", err) + } + userid := zitadel.Status.InitialAdminId + for _, u := range users.Result { + if admin.AccountName == u.UserName { + userid = u.Id + } + } + if userid == "" { + resp, err := managementClient.AddHumanUser(middleware.SetOrgID(ctx, org.Org.Id), &management.AddHumanUserRequest{ + UserName: admin.AccountName, + Profile: &management.AddHumanUserRequest_Profile{ + FirstName: admin.AccountName, + LastName: admin.AccountName, + NickName: admin.AccountName, + DisplayName: admin.AccountName, + Gender: user.Gender_GENDER_DIVERSE, + PreferredLanguage: "en", + }, + InitialPassword: password, + Email: &management.AddHumanUserRequest_Email{ + Email: "test@test.com", + IsEmailVerified: true, + }, + }) + userid = resp.UserId + if err != nil { + return ctrl.Result{}, fmt.Errorf("Error adding human user: %v", err) + } + { + + if _, err = managementClient.AddOrgMember(middleware.SetOrgID(ctx, org.Org.Id), &management.AddOrgMemberRequest{ + UserId: userid, + Roles: []string{ + "IAM_OWNER", + }, + }); err != nil { + return ctrl.Result{}, fmt.Errorf("Error adding org member: %v", err) + } + } + } + + patch := client.MergeFrom(zitadel.DeepCopy()) + 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 {