pkg/controller/direct/privilegedaccessmanager/entitlement_controller.go (401 lines of code) (raw):

// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package privilegedaccessmanager import ( "context" "fmt" "reflect" "strings" krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/privilegedaccessmanager/v1beta1" refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" gcp "cloud.google.com/go/privilegedaccessmanager/apiv1" privilegedaccessmanagerpb "cloud.google.com/go/privilegedaccessmanager/apiv1/privilegedaccessmanagerpb" "github.com/google/go-cmp/cmp" "google.golang.org/api/option" "google.golang.org/protobuf/types/known/fieldmaskpb" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" ) const ( ctrlName = "privilegedaccessmanager-controller" serviceDomain = "//privilegedaccessmanager.googleapis.com" ) func init() { registry.RegisterModel(krm.PrivilegedAccessManagerEntitlementGVK, NewModel) } func NewModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) { return &entitlementModel{config: *config}, nil } var _ directbase.Model = &entitlementModel{} type entitlementModel struct { config config.ControllerConfig } func (m *entitlementModel) client(ctx context.Context) (*gcp.Client, error) { var opts []option.ClientOption opts, err := m.config.RESTClientOptions() if err != nil { return nil, err } gcpClient, err := gcp.NewRESTClient(ctx, opts...) if err != nil { return nil, fmt.Errorf("building PrivilegedAccessManager client for Entitlement: %w", err) } return gcpClient, err } func (m *entitlementModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) { obj := &krm.PrivilegedAccessManagerEntitlement{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { return nil, fmt.Errorf("error converting to %T: %w", obj, err) } // Get ResourceID resourceID := direct.ValueOf(obj.Spec.ResourceID) if resourceID == "" { resourceID = obj.GetName() } if resourceID == "" { return nil, fmt.Errorf("cannot resolve resource ID") } container, err := oneOfContainer(ctx, reader, obj, obj.Spec.ProjectRef, obj.Spec.FolderRef, obj.Spec.OrganizationRef) if err != nil { return nil, fmt.Errorf("error resolving 'obj.Spec.ProjectRef', "+ "'obj.Spec.FolderRef' and 'obj.Spec.OrganizationRef': %w", err) } // Get location location := *obj.Spec.Location var id *PrivilegedAccessManagerEntitlementIdentity externalRef := direct.ValueOf(obj.Status.ExternalRef) if externalRef == "" { id = BuildID(container, location, resourceID) } else { id, err = asID(externalRef) if err != nil { return nil, err } if id.Parent.Container != container { return nil, fmt.Errorf("PrivilegedAccessManagerEntitlement %s/%s has parent container changed, expected %s, got %s", u.GetNamespace(), u.GetName(), id.Parent.Container, container) } if id.Parent.Location != location { return nil, fmt.Errorf("PrivilegedAccessManagerEntitlement %s/%s has spec.location changed, expect %s, got %s", u.GetNamespace(), u.GetName(), id.Parent.Location, location) } if id.Entitlement != resourceID { return nil, fmt.Errorf("PrivilegedAccessManagerEntitlement %s/%s has metadata.name or spec.resourceID changed, expect %s, got %s", u.GetNamespace(), u.GetName(), id.Entitlement, resourceID) } } if obj.Spec.RequesterJustificationConfig.NotMandatory == nil && obj.Spec.RequesterJustificationConfig.Unstructured == nil { return nil, fmt.Errorf("one and only one of 'spec.requesterJustificationConfig.notMandatory' " + "and 'spec.requesterJustificationConfig.unstructured' should be configured: neither is configured") } if obj.Spec.RequesterJustificationConfig.NotMandatory != nil && obj.Spec.RequesterJustificationConfig.Unstructured != nil { return nil, fmt.Errorf("one and only one of 'spec.requesterJustificationConfig.notMandatory' " + "and 'spec.requesterJustificationConfig.unstructured' should be configured: both configured") } // Get privilegedaccessmanager GCP client gcpClient, err := m.client(ctx) if err != nil { return nil, err } return &Adapter{ id: id, gcpClient: gcpClient, desired: obj, }, nil } func checkExactlyOneOf(values ...interface{}) (bool, interface{}) { numOfNonNil := 0 var nonNilVal interface{} for _, value := range values { if value != nil && !reflect.ValueOf(value).IsNil() { numOfNonNil++ nonNilVal = value } } if numOfNonNil != 1 { return false, nil } return true, nonNilVal } func oneOfContainer(ctx context.Context, reader client.Reader, obj *krm.PrivilegedAccessManagerEntitlement, projectRef *refs.ProjectRef, folderRef *refs.FolderRef, organizationRef *refs.OrganizationRef) (string, error) { hasExactlyOneContainer, containerRef := checkExactlyOneOf(projectRef, folderRef, organizationRef) if !hasExactlyOneContainer { return "", fmt.Errorf("exactly one of 'projectRef', 'folderRef' "+ "or 'organizationRef' must be set, but got projectRef: %+v, folderRef: %+v, organizationRef: %+v", projectRef, folderRef, organizationRef) } container := "" switch containerRef.(type) { case *refs.ProjectRef: project, err := refs.ResolveProject(ctx, reader, obj.GetNamespace(), projectRef) if err != nil { return "", err } projectID := project.ProjectID if projectID == "" { return "", fmt.Errorf("cannot resolve project: project ID is empty") } container = fmt.Sprintf("projects/%s", projectID) case *refs.FolderRef: folder, err := refs.ResolveFolder(ctx, reader, obj, folderRef) if err != nil { return "", err } folderID := folder.FolderID if folderID == "" { return "", fmt.Errorf("cannot resolve folder: folder ID is empty") } container = fmt.Sprintf("folders/%s", folderID) case *refs.OrganizationRef: organization, err := refs.ResolveOrganization(ctx, reader, obj, organizationRef) if err != nil { return "", err } organizationID := organization.OrganizationID if organizationID == "" { return "", fmt.Errorf("cannot resolve organization: organization ID is empty") } container = fmt.Sprintf("organizations/%s", organizationID) default: return "", fmt.Errorf("unexpected ref type %T", containerRef) } return container, nil } func (m *entitlementModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { // TODO: Support URLs return nil, nil } type Adapter struct { id *PrivilegedAccessManagerEntitlementIdentity gcpClient *gcp.Client desired *krm.PrivilegedAccessManagerEntitlement actual *privilegedaccessmanagerpb.Entitlement } var _ directbase.Adapter = &Adapter{} func (a *Adapter) Find(ctx context.Context) (bool, error) { log := klog.FromContext(ctx) log.V(2).Info("getting PrivilegedAccessManagerEntitlement", "name", a.id.FullyQualifiedName()) req := &privilegedaccessmanagerpb.GetEntitlementRequest{Name: a.id.FullyQualifiedName()} entitlementpb, err := a.gcpClient.GetEntitlement(ctx, req) if err != nil { if direct.IsNotFound(err) { return false, nil } return false, fmt.Errorf("error getting PrivilegedAccessManagerEntitlement %q: %w", a.id.FullyQualifiedName(), err) } a.actual = entitlementpb return true, nil } func getResourceTypeAndResourceFromContainer(container string) (string, string, error) { tokens := strings.Split(container, "/") if len(tokens) != 2 { return "", "", fmt.Errorf("container should be one of projects/<project>, "+ "folders/<folder> or organizations/<organization>, but got %s", container) } resource := fmt.Sprintf("//cloudresourcemanager.googleapis.com/%s", container) switch tokens[0] { case "projects": return "cloudresourcemanager.googleapis.com/Project", resource, nil case "folders": return "cloudresourcemanager.googleapis.com/Folder", resource, nil case "organizations": return "cloudresourcemanager.googleapis.com/Organization", resource, nil default: return "", "", fmt.Errorf("container must start with 'projects', "+ "'folders', or 'organizations', but it starts with %v", tokens[0]) } } func (a *Adapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error { u := createOp.GetUnstructured() log := klog.FromContext(ctx) log.V(2).Info("creating PrivilegedAccessManagerEntitlement", "name", a.id.FullyQualifiedName()) mapCtx := &direct.MapContext{} desired := a.desired.DeepCopy() resourceType, resource, err := getResourceTypeAndResourceFromContainer(a.id.Parent.Container) if err != nil { return fmt.Errorf("error getting resourceType and resource from container: %w", err) } hiddenFields := gcpIAMAccessResource{resourceType: resourceType, resource: resource} entitlement := PrivilegedAccessManagerEntitlementSpec_ToProto(mapCtx, &desired.Spec, hiddenFields) if mapCtx.Err() != nil { return mapCtx.Err() } req := &privilegedaccessmanagerpb.CreateEntitlementRequest{ Parent: a.id.Parent.String(), EntitlementId: a.id.Entitlement, Entitlement: entitlement, } op, err := a.gcpClient.CreateEntitlement(ctx, req) if err != nil { return fmt.Errorf("error creating PrivilegedAccessManagerEntitlement %s: %w", a.id.FullyQualifiedName(), err) } created, err := op.Wait(ctx) if err != nil { return fmt.Errorf("error waiting for PrivilegedAccessManagerEntitlement %s to be created: %w", a.id.FullyQualifiedName(), err) } log.V(2).Info("successfully created PrivilegedAccessManagerEntitlement", "name", a.id.FullyQualifiedName()) status := &krm.PrivilegedAccessManagerEntitlementStatus{} observedState := PrivilegedAccessManagerEntitlementObservedState_FromProto(mapCtx, created) if mapCtx.Err() != nil { return mapCtx.Err() } status.ObservedState = observedState status.ExternalRef = a.id.AsExternalRef() return setStatus(u, status) } func (a *Adapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error { u := updateOp.GetUnstructured() log := klog.FromContext(ctx) log.V(2).Info("updating PrivilegedAccessManagerEntitlement", "name", a.id.FullyQualifiedName()) mapCtx := &direct.MapContext{} updateMask := &fieldmaskpb.FieldMask{} parsedActual := PrivilegedAccessManagerEntitlementSpec_FromProto(mapCtx, a.actual) if mapCtx.Err() != nil { return fmt.Errorf("error generating update mask: %w", mapCtx.Err()) } sortPrincipalsInSpec(parsedActual) parsedDesired := a.desired.DeepCopy() sortPrincipalsInSpec(&parsedDesired.Spec) if !reflect.DeepEqual(parsedActual.AdditionalNotificationTargets, parsedDesired.Spec.AdditionalNotificationTargets) { log.V(2).Info("'spec.additionalNotificationTargets' field is updated (-old +new)", cmp.Diff(parsedActual.AdditionalNotificationTargets, parsedDesired.Spec.AdditionalNotificationTargets)) updateMask.Paths = append(updateMask.Paths, "additional_notification_targets") } // If '.spec.approvalWorkflow.manualApprovals.requireApproverJustification' // is unset in the desired state, the corresponding field will be 'false' in // the returned actual state when 'approvalWorkflow.manualApprovals' is not // an empty struct. So the diffing needs to be handled differently. if !(isApprovalWorkflowManualApprovalsRequireApproverJustificationUnset(&parsedDesired.Spec) && isApprovalWorkflowManualApprovalsRequireApproverJustificationSetToFalse(parsedActual)) { if !reflect.DeepEqual(parsedActual.ApprovalWorkflow, parsedDesired.Spec.ApprovalWorkflow) { log.V(2).Info("'spec.approvalWorkflow' field is updated (-old +new)", cmp.Diff(parsedActual.ApprovalWorkflow, parsedDesired.Spec.ApprovalWorkflow)) updateMask.Paths = append(updateMask.Paths, "approval_workflow") } } if !reflect.DeepEqual(parsedActual.EligibleUsers, parsedDesired.Spec.EligibleUsers) { log.V(2).Info("'spec.eligibleUsers' field is updated (-old +new)", cmp.Diff(parsedActual.EligibleUsers, parsedDesired.Spec.EligibleUsers)) updateMask.Paths = append(updateMask.Paths, "eligible_users") } if !reflect.DeepEqual(a.actual.MaxRequestDuration.AsDuration(), direct.StringDuration_ToProto(mapCtx, parsedDesired.Spec.MaxRequestDuration).AsDuration()) { log.V(2).Info("'spec.maxRequestDuration' field is updated (-old +new)", cmp.Diff(a.actual.MaxRequestDuration.AsDuration(), direct.StringDuration_ToProto(mapCtx, parsedDesired.Spec.MaxRequestDuration).AsDuration())) updateMask.Paths = append(updateMask.Paths, "max_request_duration") } if !reflect.DeepEqual(parsedActual.PrivilegedAccess, parsedDesired.Spec.PrivilegedAccess) { log.V(2).Info("'spec.privilegedAccess' field is updated (-old +new)", cmp.Diff(parsedActual.PrivilegedAccess, parsedDesired.Spec.PrivilegedAccess)) updateMask.Paths = append(updateMask.Paths, "privileged_access") } if !reflect.DeepEqual(parsedActual.RequesterJustificationConfig, parsedDesired.Spec.RequesterJustificationConfig) { log.V(2).Info("'spec.requesterJustificationConfig' field is updated (-old +new)", cmp.Diff(parsedActual.RequesterJustificationConfig, parsedDesired.Spec.RequesterJustificationConfig)) updateMask.Paths = append(updateMask.Paths, "requester_justification_config") } if len(updateMask.Paths) == 0 { log.V(2).Info("underlying PrivilegedAccessManagerEntitlement already up to date", "name", a.id.FullyQualifiedName()) status := &krm.PrivilegedAccessManagerEntitlementStatus{} observedState := PrivilegedAccessManagerEntitlementObservedState_FromProto(mapCtx, a.actual) if mapCtx.Err() != nil { return mapCtx.Err() } status.ObservedState = observedState status.ExternalRef = a.id.AsExternalRef() return setStatus(u, status) } desired := a.desired.DeepCopy() resourceType, resource, err := getResourceTypeAndResourceFromContainer(a.id.Parent.Container) if err != nil { return fmt.Errorf("error getting resourceType and resource from container: %w", err) } hiddenFields := gcpIAMAccessResource{resourceType: resourceType, resource: resource} entitlement := PrivilegedAccessManagerEntitlementSpec_ToProto(mapCtx, &desired.Spec, hiddenFields) if mapCtx.Err() != nil { return mapCtx.Err() } entitlement.Name = a.id.FullyQualifiedName() entitlement.Etag = a.actual.Etag req := &privilegedaccessmanagerpb.UpdateEntitlementRequest{ UpdateMask: updateMask, Entitlement: entitlement, } op, err := a.gcpClient.UpdateEntitlement(ctx, req) if err != nil { return fmt.Errorf("error updating PrivilegedAccessManagerEntitlement %s: %w", a.id.FullyQualifiedName(), err) } updated, err := op.Wait(ctx) if err != nil { return fmt.Errorf("error waiting for PrivilegedAccessManagerEntitlement %s to be updated: %w", a.id.FullyQualifiedName(), err) } log.V(2).Info("successfully updated PrivilegedAccessManagerEntitlement", "name", a.id.FullyQualifiedName()) status := &krm.PrivilegedAccessManagerEntitlementStatus{} observedState := PrivilegedAccessManagerEntitlementObservedState_FromProto(mapCtx, updated) if mapCtx.Err() != nil { return mapCtx.Err() } status.ObservedState = observedState return setStatus(u, status) } func isApprovalWorkflowManualApprovalsRequireApproverJustificationUnset(spec *krm.PrivilegedAccessManagerEntitlementSpec) bool { return spec.ApprovalWorkflow == nil || spec.ApprovalWorkflow.ManualApprovals == nil || spec.ApprovalWorkflow.ManualApprovals.RequireApproverJustification == nil } func isApprovalWorkflowManualApprovalsRequireApproverJustificationSetToFalse(spec *krm.PrivilegedAccessManagerEntitlementSpec) bool { return spec.ApprovalWorkflow != nil && spec.ApprovalWorkflow.ManualApprovals != nil && spec.ApprovalWorkflow.ManualApprovals.RequireApproverJustification != nil && *spec.ApprovalWorkflow.ManualApprovals.RequireApproverJustification == false } func (a *Adapter) Export(ctx context.Context) (*unstructured.Unstructured, error) { if a.actual == nil { return nil, fmt.Errorf("Find() not called") } u := &unstructured.Unstructured{} obj := &krm.PrivilegedAccessManagerEntitlement{} mapCtx := &direct.MapContext{} obj.Spec = direct.ValueOf(PrivilegedAccessManagerEntitlementSpec_FromProto(mapCtx, a.actual)) if mapCtx.Err() != nil { return nil, mapCtx.Err() } if strings.HasPrefix(a.id.Parent.Container, "projects") { obj.Spec.ProjectRef = &refs.ProjectRef{External: a.id.Parent.Container} } else if strings.HasPrefix(a.id.Parent.Container, "folders") { obj.Spec.FolderRef = &refs.FolderRef{External: a.id.Parent.Container} } else { obj.Spec.OrganizationRef = &refs.OrganizationRef{External: a.id.Parent.Container} } obj.Spec.Location = &a.id.Parent.Location uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { return nil, err } u.Object = uObj return u, nil } // Delete implements the Adapter interface. func (a *Adapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) { log := klog.FromContext(ctx) log.V(2).Info("deleting PrivilegedAccessManagerEntitlement", "name", a.id.FullyQualifiedName()) req := &privilegedaccessmanagerpb.DeleteEntitlementRequest{Name: a.id.FullyQualifiedName()} op, err := a.gcpClient.DeleteEntitlement(ctx, req) if err != nil { if direct.IsNotFound(err) { return false, nil } return false, fmt.Errorf("error deleting PrivilegedAccessManagerEntitlement %s: %w", a.id.FullyQualifiedName(), err) } _, err = op.Wait(ctx) if err != nil { return false, fmt.Errorf("error waiting for PrivilegedAccessManagerEntitlement %s to be deleted: %w", a.id.FullyQualifiedName(), err) } log.V(2).Info("successfully deleted PrivilegedAccessManagerEntitlement", "name", a.id.FullyQualifiedName()) return true, nil } func setStatus(u *unstructured.Unstructured, typedStatus any) error { status, err := runtime.DefaultUnstructuredConverter.ToUnstructured(typedStatus) if err != nil { return fmt.Errorf("error converting status to unstructured: %w", err) } old, _, _ := unstructured.NestedMap(u.Object, "status") if old != nil { status["conditions"] = old["conditions"] status["observedGeneration"] = old["observedGeneration"] status["externalRef"] = old["externalRef"] } u.Object["status"] = status return nil }