From 74ac1e33a4081ea361395e36fca6e7a868b51b38 Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Mon, 20 May 2024 15:54:56 -0500 Subject: [PATCH] Add useer grants [ZITADOPER-1] --- ops/chart/crds/machineuser-crd.yaml | 48 ++++++++++ src/api/v1alpha1/machineuser_types.go | 8 +- .../zitadel.topmanage.com_machineusers.yaml | 48 ++++++++++ .../controller/machineuser_controller.go | 94 +++++++++++++++++++ 4 files changed, 197 insertions(+), 1 deletion(-) diff --git a/ops/chart/crds/machineuser-crd.yaml b/ops/chart/crds/machineuser-crd.yaml index 4e7ad1c..9212805 100644 --- a/ops/chart/crds/machineuser-crd.yaml +++ b/ops/chart/crds/machineuser-crd.yaml @@ -77,6 +77,54 @@ spec: type: string type: object x-kubernetes-map-type: atomic + userGrants: + items: + properties: + projectRef: + 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: + - projectRef + type: object + type: array required: - accessTokenType - organizationRef diff --git a/src/api/v1alpha1/machineuser_types.go b/src/api/v1alpha1/machineuser_types.go index d2c2a94..4a64889 100644 --- a/src/api/v1alpha1/machineuser_types.go +++ b/src/api/v1alpha1/machineuser_types.go @@ -27,6 +27,11 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +type UserGrant struct { + ProjectRef ProjectRef `json:"projectRef"` + RoleKeys []string `json:"roleKeys,omitempty"` +} + // MachineUserSpec defines the desired state of MachineUser type MachineUserSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster @@ -35,7 +40,8 @@ type MachineUserSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec OrganizationRef OrganizationRef `json:"organizationRef" webhook:"inmutable"` // +kubebuilder:validation:Enum=ACCESS_TOKEN_TYPE_BEARER;ACCESS_TOKEN_TYPE_JWT - AccessTokenType string `json:"accessTokenType"` + AccessTokenType string `json:"accessTokenType"` + UserGrants []UserGrant `json:"userGrants,omitempty"` } // MachineUserStatus defines the observed state of MachineUser diff --git a/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml b/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml index 53f1878..494c2ea 100644 --- a/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml +++ b/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml @@ -78,6 +78,54 @@ spec: type: string type: object x-kubernetes-map-type: atomic + userGrants: + items: + properties: + projectRef: + 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: + - projectRef + type: object + type: array required: - accessTokenType - organizationRef diff --git a/src/internal/controller/machineuser_controller.go b/src/internal/controller/machineuser_controller.go index b650be5..5310d3b 100644 --- a/src/internal/controller/machineuser_controller.go +++ b/src/internal/controller/machineuser_controller.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "reflect" + "sort" "strings" "time" @@ -14,6 +16,8 @@ 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" + object "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/object" + project "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/project" user "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/user" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" @@ -103,6 +107,10 @@ func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient Name: "jwt", Reconcile: wr.reconcileJWT, }, + { + Name: "usergrants", + Reconcile: wr.reconcileUserGrants, + }, } for _, p := range phases { err := p.Reconcile(ctx, ztdClient) @@ -275,6 +283,92 @@ func (wr *wrappedMachineUserReconciler) reconcileJWT(ctx context.Context, ztdCli return nil } +func (wr *wrappedMachineUserReconciler) reconcileUserGrants(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.MachineUser.Spec.OrganizationRef, wr.MachineUser.Namespace) + if err != nil { + return err + } + existingUserGrants, err := ztdClient.ListUserGrants(ctx, &pb.ListUserGrantRequest{ + Queries: []*user.UserGrantQuery{ + { + Query: &user.UserGrantQuery_UserIdQuery{ + UserIdQuery: &user.UserGrantUserIDQuery{ + UserId: wr.MachineUser.Status.UserId, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("Error listing MachineUser grants: %v", err) + } + ctx = middleware.SetOrgID(ctx, org.Status.OrgId) + for _, userGrant := range wr.MachineUser.DeepCopy().Spec.UserGrants { + userGrantedProject, err := wr.refResolver.ProjectRef(ctx, &userGrant.ProjectRef, wr.MachineUser.Namespace) + if err != nil { + return err + } + var existingUserGrant *user.UserGrant + for _, eGrant := range existingUserGrants.Result { + if eGrant.ProjectId == userGrantedProject.Status.ProjectId { + existingUserGrant = eGrant + break + } + } + if existingUserGrant == nil { + grantedProjects, err := ztdClient.ListGrantedProjects(ctx, &pb.ListGrantedProjectsRequest{ + Queries: []*project.ProjectQuery{ + { + Query: &project.ProjectQuery_NameQuery{ + NameQuery: &project.ProjectNameQuery{ + Name: userGrantedProject.Name, + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("Error listing granted projects: %v", err) + } + + var existingProjectGrant *project.GrantedProject + for _, existingGrantedProject := range grantedProjects.Result { + if existingGrantedProject.ProjectId == userGrantedProject.Status.ProjectId { + existingGrantedProject = existingGrantedProject + } + } + if existingProjectGrant == nil { + return fmt.Errorf("Error no project granted to user organization: %v", err) + } + _, err = ztdClient.AddUserGrant(ctx, &pb.AddUserGrantRequest{ + UserId: wr.MachineUser.Status.UserId, + RoleKeys: userGrant.RoleKeys, + ProjectId: existingProjectGrant.ProjectId, + ProjectGrantId: existingProjectGrant.GrantId, + }) + if err != nil { + return fmt.Errorf("Error Adding MachineUser grant: %v", err) + } + + } else { + sort.Strings(existingUserGrant.RoleKeys) + sort.Strings(userGrant.RoleKeys) + if !reflect.DeepEqual(existingUserGrant.RoleKeys, userGrant.RoleKeys) { + _, err := ztdClient.UpdateUserGrant(ctx, &pb.UpdateUserGrantRequest{ + UserId: wr.MachineUser.Status.UserId, + GrantId: existingUserGrant.Id, + RoleKeys: userGrant.RoleKeys, + }) + if err != nil { + return fmt.Errorf("Error Updating MachineUser grant: %v", err) + } + } + } + } + return nil +} + func (wr *wrappedMachineUserReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { patch := client.MergeFrom(wr.MachineUser.DeepCopy()) patcher(&wr.MachineUser.Status)