package controller import ( "context" "fmt" "slices" "time" zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" "github.com/HaimKortovich/zitadel-resources-operator/pkg/builder" condition "github.com/HaimKortovich/zitadel-resources-operator/pkg/condition" "github.com/HaimKortovich/zitadel-resources-operator/pkg/controller/core" clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2" "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/internal_permission/v2" "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/object/v2" "google.golang.org/protobuf/types/known/timestamppb" user "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2" 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" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // 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 := core.NewCoreFinalizer(r.Client, wf) tr := core.NewCoreReconciler(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) core.WrappedCoreReconciler { return &wrappedMachineUserReconciler{ Client: client, refResolver: refResolver, MachineUser: MachineUser, Builder: builder, } } type machineUserReconcilePhase struct { Name string Reconcile func(context.Context, *clientv2.Client) error } func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { phases := []machineUserReconcilePhase{ { Name: "machineUser", Reconcile: wr.reconcileMachineUser, }, { Name: "internalPermissions", Reconcile: wr.reconcileInternalPermissions, }, { 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 *clientv2.Client) error { org, err := wr.refResolver.OrganizationRef(ctx, &wr.MachineUser.Spec.OrganizationRef, wr.MachineUser.Namespace) if err != nil { return err } if org.Status.OrganizationId == nil { return fmt.Errorf("Organization not created yet") } var userId *string userList, err := ztdClient.UserServiceV2().ListUsers(ctx, &user.ListUsersRequest{ Queries: []*user.SearchQuery{{ Query: &user.SearchQuery_AndQuery{ AndQuery: &user.AndQuery{ Queries: []*user.SearchQuery{ &user.SearchQuery{ Query: &user.SearchQuery_UserNameQuery{ UserNameQuery: &user.UserNameQuery{ UserName: wr.MachineUser.Spec.Username, Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, }, }, }, &user.SearchQuery{ Query: &user.SearchQuery_OrganizationIdQuery{ OrganizationIdQuery: &user.OrganizationIdQuery{ OrganizationId: *org.Status.OrganizationId, }, }, }, }, }, }, }}}) if err != nil { return fmt.Errorf("error listing users: %v", err) } if len(userList.Result) > 0 { userId = &userList.Result[0].UserId } if userId == nil { accesTokenType := user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER switch wr.MachineUser.Spec.AccessTokenType { case "ACCESS_TOKEN_TYPE_BEARER": accesTokenType = user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER case "ACCESS_TOKEN_TYPE_JWT": accesTokenType = user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT } ztdClient.UserServiceV2().CreateUser(ctx, &user.CreateUserRequest{ OrganizationId: *org.Status.OrganizationId, Username: &wr.MachineUser.Spec.Username, UserType: &user.CreateUserRequest_Machine_{ Machine: &user.CreateUserRequest_Machine{ Name: wr.MachineUser.Spec.Username, AccessTokenType: accesTokenType, }, }, }) } 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 *clientv2.Client) error { pats, err := ztdClient.UserServiceV2().ListPersonalAccessTokens(ctx, &user.ListPersonalAccessTokensRequest{ Filters: []*user.PersonalAccessTokensSearchFilter{ { Filter: &user.PersonalAccessTokensSearchFilter_UserIdFilter{ UserIdFilter: &filter.IDFilter{ Id: *wr.MachineUser.Status.UserId, }, }, }, }}) if err != nil { return fmt.Errorf("Error getting PAT: %v", err) } if pats.Result == nil || !wr.MachineUser.Status.GetConditionStatus(zitadelv1alpha1.ConditionTypePATUpToDate) { resp, err := ztdClient.UserServiceV2().AddPersonalAccessToken(ctx, &user.AddPersonalAccessTokenRequest{ UserId: *wr.MachineUser.Status.UserId, ExpirationDate: timestamppb.New(time.Now().AddDate(999, 1, 1)), }) 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) { if err := wr.Create(ctx, desiredPatSecret); err != nil { return fmt.Errorf("error creating PAT Secret: %v", err) } } else { return fmt.Errorf("error getting PAT Secret: %v", err) } } else { patch := client.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) reconcileInternalPermissions(ctx context.Context, ztdClient *clientv2.Client) error { for _, permission := range wr.MachineUser.Spec.InternalPermissions { permissionPbFilter := &internal_permission.AdministratorSearchFilter_Resource{ Resource: &internal_permission.ResourceFilter{}, } permissionPb := &internal_permission.ResourceType{} if permission.Resource.Instance != nil { permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_Instance{Instance: true} permissionPb.Resource = &internal_permission.ResourceType_Instance{Instance: true} } else if permission.Resource.Organization != nil { permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_OrganizationId{OrganizationId: permission.Resource.Organization.OrgID} permissionPb.Resource = &internal_permission.ResourceType_OrganizationId{OrganizationId: permission.Resource.Organization.OrgID} } else if permission.Resource.ProjectGrant != nil { permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_ProjectGrant_{ProjectGrant: &internal_permission.ResourceFilter_ProjectGrant{ ProjectId: permission.Resource.ProjectGrant.ProjectID, OrganizationId: permission.Resource.ProjectGrant.ProjectID, }} permissionPb.Resource = &internal_permission.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: permission.Resource.ProjectGrant.ProjectID, OrganizationId: permission.Resource.ProjectGrant.ProjectID, }} } else if permission.Resource.Project != nil { permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_ProjectId{ProjectId: permission.Resource.Project.ProjectID} permissionPb.Resource = &internal_permission.ResourceType_ProjectId{ProjectId: permission.Resource.Project.ProjectID} } var admin *internal_permission.Administrator adminRoleList, err := ztdClient.InternalPermissionServiceV2().ListAdministrators(ctx, &internal_permission.ListAdministratorsRequest{ Filters: []*internal_permission.AdministratorSearchFilter{ { Filter: permissionPbFilter, }, { Filter: &internal_permission.AdministratorSearchFilter_And{And: &internal_permission.AndFilter{Queries: []*internal_permission.AdministratorSearchFilter{ { Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ InUserIdsFilter: &filter.InIDsFilter{ Ids: []string{ *wr.MachineUser.Status.UserId, }, }, }, }}, }, }, }, }, }) if err != nil { return fmt.Errorf("error listing admin list: %v", err) } if len(adminRoleList.Administrators) > 0 { admin = adminRoleList.Administrators[0] } if admin == nil { _, err := ztdClient.InternalPermissionServiceV2().CreateAdministrator(ctx, &internal_permission.CreateAdministratorRequest{ UserId: *wr.MachineUser.Status.UserId, Roles: permission.Roles, Resource: permissionPb, }) if err != nil { return fmt.Errorf("error creating admin: %v", err) } } else { uniqueRoles := permission.Roles uniqueRoles = append(uniqueRoles, admin.Roles...) uniqueRoles = slices.Compact(uniqueRoles) _, err := ztdClient.InternalPermissionServiceV2().UpdateAdministrator(ctx, &internal_permission.UpdateAdministratorRequest{ UserId: *wr.MachineUser.Status.UserId, Roles: uniqueRoles, Resource: permissionPb, }) if err != nil { return fmt.Errorf("error updating admin: %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.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). Complete(r) }