From 626e33a7737c6804431dcaed305e54b14665290c Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Fri, 17 May 2024 13:51:08 -0500 Subject: [PATCH] Add grants to project [ZITADOPER-1] --- ops/chart/crds/project-crd.yaml | 49 ++++++++++++++++ src/api/v1alpha1/project_types.go | 6 ++ src/api/v1alpha1/zz_generated.deepcopy.go | 28 +++++++++ .../bases/zitadel.topmanage.com_projects.yaml | 49 ++++++++++++++++ src/internal/controller/project_controller.go | 58 +++++++++++++++++++ 5 files changed, 190 insertions(+) diff --git a/ops/chart/crds/project-crd.yaml b/ops/chart/crds/project-crd.yaml index 70a7a31..56ca4eb 100644 --- a/ops/chart/crds/project-crd.yaml +++ b/ops/chart/crds/project-crd.yaml @@ -34,6 +34,55 @@ spec: spec: description: ProjectSpec defines the desired state of Project properties: + grants: + items: + properties: + organizationRef: + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this pod). + This syntax is chosen only to have some well-defined way + of referencing a part of an object. TODO: this design + is not final and this field is subject to change in the + future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + roleKeys: + items: + type: string + type: array + required: + - organizationRef + - roleKeys + type: object + type: array hasProjectCheck: type: boolean organizationRef: diff --git a/src/api/v1alpha1/project_types.go b/src/api/v1alpha1/project_types.go index 8926db4..a84dd7d 100644 --- a/src/api/v1alpha1/project_types.go +++ b/src/api/v1alpha1/project_types.go @@ -32,6 +32,10 @@ type Role struct { DisplayName string `json:"displayName"` Group string `json:"group"` } +type Grant struct { + OrganizationRef OrganizationRef `json:"organizationRef"` + RoleKeys []string `json:"roleKeys"` +} // ProjectSpec defines the desired state of Project type ProjectSpec struct { @@ -44,6 +48,8 @@ type ProjectSpec struct { // +optional Roles []Role `json:"roles"` // +optional + Grants []Grant `json:"grants"` + // +optional ProjectRoleAssertion bool `json:"projectRoleAssertion,omitempty"` // +optional ProjectRoleCheck bool `json:"projectRoleCheck,omitempty"` diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index 2e1b39a..9b2142c 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -154,6 +154,27 @@ func (in *DomainSettings) DeepCopy() *DomainSettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Grant) DeepCopyInto(out *Grant) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.RoleKeys != nil { + in, out := &in.RoleKeys, &out.RoleKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Grant. +func (in *Grant) DeepCopy() *Grant { + if in == nil { + return nil + } + out := new(Grant) + 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 @@ -606,6 +627,13 @@ func (in *ProjectSpec) DeepCopyInto(out *ProjectSpec) { *out = make([]Role, len(*in)) copy(*out, *in) } + if in.Grants != nil { + in, out := &in.Grants, &out.Grants + *out = make([]Grant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSpec. diff --git a/src/config/crd/bases/zitadel.topmanage.com_projects.yaml b/src/config/crd/bases/zitadel.topmanage.com_projects.yaml index 8e734ae..5a54387 100644 --- a/src/config/crd/bases/zitadel.topmanage.com_projects.yaml +++ b/src/config/crd/bases/zitadel.topmanage.com_projects.yaml @@ -35,6 +35,55 @@ spec: spec: description: ProjectSpec defines the desired state of Project properties: + grants: + items: + properties: + organizationRef: + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this pod). + This syntax is chosen only to have some well-defined way + of referencing a part of an object. TODO: this design + is not final and this field is subject to change in the + future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + roleKeys: + items: + type: string + type: array + required: + - organizationRef + - roleKeys + type: object + type: array hasProjectCheck: type: boolean organizationRef: diff --git a/src/internal/controller/project_controller.go b/src/internal/controller/project_controller.go index c8e9b6f..52c73c7 100644 --- a/src/internal/controller/project_controller.go +++ b/src/internal/controller/project_controller.go @@ -19,6 +19,8 @@ package controller import ( "context" "fmt" + "reflect" + "sort" "strings" "time" @@ -28,6 +30,7 @@ import ( "github.com/zitadel/zitadel-go/v2/pkg/client/management" "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" + "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/project" "golang.org/x/exp/maps" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" @@ -107,6 +110,10 @@ func (wr *wrappedProjectReconciler) Reconcile(ctx context.Context, ztdClient *ma Name: "roles", Reconcile: wr.reconcileRoles, }, + { + Name: "grants", + Reconcile: wr.reconcileGrants, + }, } for _, p := range phases { err := p.Reconcile(ctx, ztdClient) @@ -219,6 +226,57 @@ func (wr *wrappedProjectReconciler) reconcileRoles(ctx context.Context, ztdClien return nil } +func (wr *wrappedProjectReconciler) reconcileGrants(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace) + if err != nil { + return err + } + existingGrants, err := ztdClient.ListProjectGrants(ctx, &pb.ListProjectGrantsRequest{ + ProjectId: wr.project.Status.ProjectId, + }) + if err != nil { + return fmt.Errorf("Error listing project grants: %v", err) + } + ctx = middleware.SetOrgID(ctx, org.Status.OrgId) + for _, grant := range wr.project.DeepCopy().Spec.Grants { + grantedOrg, err := wr.refResolver.OrganizationRef(ctx, &grant.OrganizationRef, wr.project.Namespace) + if err != nil { + return err + } + var existingGrant *project.GrantedProject + for _, eGrant := range existingGrants.Result { + if eGrant.GrantedOrgId == grantedOrg.Status.OrgId { + existingGrant = eGrant + break + } + } + if existingGrant == nil { + _, err := ztdClient.AddProjectGrant(ctx, &pb.AddProjectGrantRequest{ + ProjectId: wr.project.Status.ProjectId, + GrantedOrgId: grantedOrg.Status.OrgId, + RoleKeys: grant.RoleKeys, + }) + if err != nil { + return fmt.Errorf("Error Adding project grant: %v", err) + } + } else { + sort.Strings(existingGrant.GrantedRoleKeys) + sort.Strings(grant.RoleKeys) + if !reflect.DeepEqual(existingGrant.GrantedRoleKeys, grant.RoleKeys) { + _, err := ztdClient.UpdateProjectGrant(ctx, &pb.UpdateProjectGrantRequest{ + ProjectId: wr.project.Status.ProjectId, + GrantId: existingGrant.GrantId, + RoleKeys: grant.RoleKeys, + }) + if err != nil { + return fmt.Errorf("Error Updating project grant: %v", err) + } + } + } + } + return nil +} + func (wr *wrappedProjectReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { patch := client.MergeFrom(wr.project.DeepCopy()) patcher(&wr.project.Status)