package controller import ( "context" "encoding/json" "fmt" "reflect" "sort" "strings" "time" zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" "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/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" 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/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" clientpkg "sigs.k8s.io/controller-runtime/pkg/client" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" ) // MachineUserReconciler reconciles a MachineUser object type MachineUserReconciler struct { client.Client RefResolver *zitadelv1alpha1.RefResolver ConditionReady *condition.Ready RequeueInterval time.Duration Builder *builder.Builder } func NewMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, requeueInterval time.Duration) *MachineUserReconciler { return &MachineUserReconciler{ Client: client, RefResolver: refResolver, ConditionReady: conditionReady, RequeueInterval: requeueInterval, Builder: builder, } } //+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers/status,verbs=get;update;patch //+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *MachineUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var MachineUser zitadelv1alpha1.MachineUser if err := r.Get(ctx, req.NamespacedName, &MachineUser); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } wr := newWrappedMachineUserReconciler(r.Client, r.RefResolver, r.Builder, &MachineUser) wf := newWrappedMachineUserFinalizer(r.Client, &MachineUser) tf := zitadel.NewZitadelFinalizer(r.Client, wf) tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) result, err := tr.Reconcile(ctx, &MachineUser) if err != nil { return result, fmt.Errorf("error reconciling in MachineUserReconciler: %v", err) } return result, nil } type wrappedMachineUserReconciler struct { client.Client refResolver *zitadelv1alpha1.RefResolver MachineUser *zitadelv1alpha1.MachineUser Builder *builder.Builder } func newWrappedMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, MachineUser *zitadelv1alpha1.MachineUser) zitadel.WrappedReconciler { return &wrappedMachineUserReconciler{ Client: client, refResolver: refResolver, MachineUser: MachineUser, Builder: builder, } } type machineUserReconcilePhase struct { Name string Reconcile func(context.Context, *management.Client) error } func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { phases := []machineUserReconcilePhase{ { Name: "machineUser", Reconcile: wr.reconcileMachineUser, }, { Name: "usergrants", Reconcile: wr.reconcileUserGrants, }, { Name: "pat", Reconcile: wr.reconcilePAT, }, { Name: "jwt", Reconcile: wr.reconcileJWT, }, } for _, p := range phases { err := p.Reconcile(ctx, ztdClient) if err != nil { return err } } return nil } func (wr *wrappedMachineUserReconciler) reconcileMachineUser(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 } zitadel, err := wr.refResolver.ZitadelCluster(ctx, &org.Spec.ZitadelClusterRef, wr.MachineUser.Namespace) if err != nil { return err } machineUser, err := ztdClient.GetUserByLoginNameGlobal(ctx, &pb.GetUserByLoginNameGlobalRequest{ LoginName: strings.ToLower(fmt.Sprintf("%s@%s.%s", wr.MachineUser.Name, org.Name, zitadel.Spec.Host)), }) if err != nil { if !strings.Contains(err.Error(), "could not be found") { return fmt.Errorf("Error getting machineuser: %v", err) } } var userid string if machineUser == nil { resp, err := ztdClient.AddMachineUser(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.AddMachineUserRequest{ Name: wr.MachineUser.Name, UserName: wr.MachineUser.Name, Description: wr.MachineUser.Name, AccessTokenType: user.AccessTokenType(user.AccessTokenType_value[wr.MachineUser.Spec.AccessTokenType]), }, ) if err != nil { return fmt.Errorf("error creating MachineUser in Zitadel: %v", err) } userid = resp.UserId } else { _, err = ztdClient.UpdateMachine(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateMachineRequest{ UserId: machineUser.User.Id, 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(), "User.NotChanged ") { return fmt.Errorf("Error updating MchineUser: %v", err) } } userid = machineUser.User.Id } patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) wr.MachineUser.Status.UserId = userid return wr.Client.Status().Patch(ctx, wr.MachineUser, patch) } func (wr *wrappedMachineUserReconciler) reconcilePAT(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 } ctx = middleware.SetOrgID(ctx, org.Status.OrgId) token, err := ztdClient.GetPersonalAccessTokenByIDs(ctx, &pb.GetPersonalAccessTokenByIDsRequest{ UserId: wr.MachineUser.Status.UserId, TokenId: wr.MachineUser.Status.PATId, }) if err != nil { if !(strings.Contains(err.Error(), "NotFound") || strings.Contains(err.Error(), "length must be between 1 and 200 runes")) { return fmt.Errorf("Error getting PAT: %v", err) } } if token == nil || !wr.MachineUser.Status.GetConditionStatus(zitadelv1alpha1.ConditionTypePATUpToDate) { if token != nil { if _, err = ztdClient.RemovePersonalAccessToken(ctx, &pb.RemovePersonalAccessTokenRequest{ UserId: wr.MachineUser.Status.UserId, TokenId: wr.MachineUser.Status.PATId, }); err != nil { return fmt.Errorf("Error removing PAT: %v", err) } } resp, err := ztdClient.AddPersonalAccessToken(ctx, &pb.AddPersonalAccessTokenRequest{ UserId: wr.MachineUser.Status.UserId, }) if err != nil { return fmt.Errorf("Error adding PAT: %v", err) } key := types.NamespacedName{ Name: wr.MachineUser.PatSecretName(), Namespace: wr.MachineUser.Namespace, } desiredPatSecret, err := wr.Builder.BuildSecret(builder.SecretOpts{ Key: key, Immutable: false, Data: map[string][]byte{ "pat": []byte(resp.Token), }, }, wr.MachineUser) if err != nil { return fmt.Errorf("error building PAT Secret: %v", err) } { var existingPatSecret corev1.Secret if err := wr.Get(ctx, key, &existingPatSecret); err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("error getting PAT Secret: %v", err) } if err := wr.Create(ctx, desiredPatSecret); err != nil { return fmt.Errorf("error creating PAT Secret: %v", err) } } patch := clientpkg.MergeFrom(existingPatSecret.DeepCopy()) existingPatSecret.Data = desiredPatSecret.Data if err = wr.Patch(ctx, &existingPatSecret, patch); err != nil { return err } } if err = wr.PatchStatus(ctx, condition.SetPatUpToDate); err != nil { return err } patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) wr.MachineUser.Status.PATId = resp.TokenId return wr.Client.Status().Patch(ctx, wr.MachineUser, patch) } return nil } func (wr *wrappedMachineUserReconciler) reconcileJWT(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 } ctx = middleware.SetOrgID(ctx, org.Status.OrgId) token, err := ztdClient.GetMachineKeyByIDs(ctx, &pb.GetMachineKeyByIDsRequest{ UserId: wr.MachineUser.Status.UserId, KeyId: wr.MachineUser.Status.KeyId, }) if err != nil { if !(strings.Contains(err.Error(), "NotFound") || strings.Contains(err.Error(), "length must be between 1 and 200 runes")) { return fmt.Errorf("Error getting JWT: %v", err) } } if token == nil { resp, err := ztdClient.AddMachineKey(ctx, &pb.AddMachineKeyRequest{ UserId: wr.MachineUser.Status.UserId, Type: authn.KeyType_KEY_TYPE_JSON, }) if err != nil { return fmt.Errorf("Error adding JWT: %v", err) } key := types.NamespacedName{ Name: wr.MachineUser.JWTSecretName(), Namespace: wr.MachineUser.Namespace, } var jsonKey Key if err = json.Unmarshal(resp.KeyDetails, &jsonKey); err != nil { return fmt.Errorf("Could not unmarshal key details: %v", err) } secretData := map[string][]byte{ "clientId": []byte(jsonKey.ClientID), "type": []byte(jsonKey.Type), "keyId": []byte(jsonKey.KeyID), "appId": []byte(jsonKey.AppID), "key": []byte(jsonKey.Key), } jwtSecret, err := wr.Builder.BuildSecret(builder.SecretOpts{ Key: key, Immutable: false, Data: secretData, }, wr.MachineUser) if err != nil { return fmt.Errorf("error building machine key Secret: %v", err) } if err := wr.Create(ctx, jwtSecret); err != nil { return fmt.Errorf("error creating machine key Secret: %v", err) } patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) wr.MachineUser.Status.KeyId = resp.KeyId return wr.Client.Status().Patch(ctx, wr.MachineUser, patch) } 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 } ctx = middleware.SetOrgID(ctx, org.Status.OrgId) existingUserGrants, err := ztdClient.ListUserGrants(ctx, &pb.ListUserGrantRequest{ Queries: []*user.UserGrantQuery{ { Query: &user.UserGrantQuery_UserIdQuery{ UserIdQuery: &user.UserGrantUserIDQuery{ UserId: wr.MachineUser.Status.UserId, }, }, }, { Query: &user.UserGrantQuery_WithGrantedQuery{ WithGrantedQuery: &user.UserGrantWithGrantedQuery{ WithGranted: true, }, }, }, }, }) if err != nil { return fmt.Errorf("Error listing MachineUser grants: %v", err) } 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 && eGrant.UserId == wr.MachineUser.Status.UserId { 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 { existingProjectGrant = existingGrantedProject break } } if existingProjectGrant == nil { return fmt.Errorf("Error no project granted to user organization") } if err = wr.PatchStatus(ctx, condition.SetPatOutOfDate); err != nil { return 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) { if err = wr.PatchStatus(ctx, condition.SetPatOutOfDate); err != nil { return err } _, 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) if err := wr.Client.Status().Patch(ctx, wr.MachineUser, patch); err != nil { return fmt.Errorf("error patching MachineUser status: %v", err) } return nil } // SetupWithManager sets up the controller with the Manager. func (r *MachineUserReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&zitadelv1alpha1.MachineUser{}). Owns(&corev1.Secret{}). WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}). Complete(r) }