diff --git a/cmd/milo/controller-manager/controllermanager.go b/cmd/milo/controller-manager/controllermanager.go index 3a02cbdf..1e27052e 100644 --- a/cmd/milo/controller-manager/controllermanager.go +++ b/cmd/milo/controller-manager/controllermanager.go @@ -518,7 +518,7 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { logger.Error(err, "Error setting up organizationmembership webhook") klog.FlushAndExit(klog.ExitFlushTimeout, 1) } - if err := iamv1alpha1webhook.SetupUserWebhooksWithManager(ctrl, SystemNamespace, "iam-user-self-manage"); err != nil { + if err := iamv1alpha1webhook.SetupUserWebhooksWithManager(ctrl, SystemNamespace, "iam-user-self-manage", "activity-self-audit-log-querier"); err != nil { logger.Error(err, "Error setting up user webhook") klog.FlushAndExit(klog.ExitFlushTimeout, 1) } diff --git a/config/services/activity/kustomization.yaml b/config/services/activity/kustomization.yaml index 729d029c..278cdc83 100644 --- a/config/services/activity/kustomization.yaml +++ b/config/services/activity/kustomization.yaml @@ -6,5 +6,8 @@ apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component +resources: + - roles/activity-self-audit-log-querier.yaml + components: - policies diff --git a/config/services/activity/roles/activity-self-audit-log-querier.yaml b/config/services/activity/roles/activity-self-audit-log-querier.yaml new file mode 100644 index 00000000..969ef3f4 --- /dev/null +++ b/config/services/activity/roles/activity-self-audit-log-querier.yaml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: AGPL-3.0-only + +# Self-scoped audit-log query capability, owned entirely by the activity +# service overlay. A distinct Role (not a partial patch of iam-user-self-manage), +# so it is applied by a single field manager and never collides with the core +# role bundle. Granted per-user, scoped to the user's own User resource, by the +# user webhook (mirrors the existing user-self-manage PolicyBinding). +apiVersion: iam.miloapis.com/v1alpha1 +kind: Role +metadata: + name: activity-self-audit-log-querier + annotations: + kubernetes.io/display-name: Self Audit Log Querier + kubernetes.io/description: "Allows a user to query their own audit logs." +spec: + launchStage: Beta + inheritedRoles: + - name: activity.miloapis.com-audit-log-querier diff --git a/internal/controllers/iam/user_controller.go b/internal/controllers/iam/user_controller.go index 8b619d0c..40a7a001 100644 --- a/internal/controllers/iam/user_controller.go +++ b/internal/controllers/iam/user_controller.go @@ -224,6 +224,24 @@ func (r *UserController) ensureOwnerReferences(ctx context.Context, user *iamv1a log.Info("Updated PolicyBinding with owner reference", "user", user.Name) } + // Update self audit-log PolicyBinding (only present when the activity service + // overlay is deployed; otherwise the webhook skips creating it). + auditLogPolicyBindingName := fmt.Sprintf("user-self-audit-log-%s", user.Name) + auditLogPolicyBinding := &iamv1alpha1.PolicyBinding{} + err = r.Client.Get(ctx, types.NamespacedName{Name: auditLogPolicyBindingName, Namespace: "milo-system"}, auditLogPolicyBinding) + if apierrors.IsNotFound(err) { + // Binding absent: activity overlay not deployed, or webhook hasn't created it yet. + log.Info("Self audit-log PolicyBinding not found, skipping", "user", user.Name, "policyBinding", auditLogPolicyBindingName) + } else if err != nil { + return fmt.Errorf("failed to get self audit-log policy binding: %w", err) + } else if !hasOwnerReference(auditLogPolicyBinding.OwnerReferences, ownerRef) { + auditLogPolicyBinding.OwnerReferences = append(auditLogPolicyBinding.OwnerReferences, ownerRef) + if err := r.Client.Update(ctx, auditLogPolicyBinding); err != nil { + return fmt.Errorf("failed to update self audit-log policy binding with owner reference: %w", err) + } + log.Info("Updated self audit-log PolicyBinding with owner reference", "user", user.Name) + } + // Update UserPreference userPreferenceName := fmt.Sprintf("userpreference-%s", user.Name) userPreference := &iamv1alpha1.UserPreference{} diff --git a/internal/webhooks/iam/v1alpha1/user_webhook.go b/internal/webhooks/iam/v1alpha1/user_webhook.go index 13393d1d..1317ef3c 100644 --- a/internal/webhooks/iam/v1alpha1/user_webhook.go +++ b/internal/webhooks/iam/v1alpha1/user_webhook.go @@ -26,7 +26,7 @@ var userlog = logf.Log.WithName("user-resource") // +kubebuilder:webhook:path=/validate-iam-miloapis-com-v1alpha1-user,mutating=false,failurePolicy=fail,sideEffects=NoneOnDryRun,groups=iam.miloapis.com,resources=users,verbs=create;update,versions=v1alpha1,name=vuser.iam.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system // SetupWebhooksWithManager sets up all iam.miloapis.com webhooks -func SetupUserWebhooksWithManager(mgr ctrl.Manager, systemNamespace string, userSelfManageRoleName string) error { +func SetupUserWebhooksWithManager(mgr ctrl.Manager, systemNamespace string, userSelfManageRoleName string, selfAuditLogRoleName string) error { userlog.Info("Setting up iam.miloapis.com user webhooks") return ctrl.NewWebhookManagedBy(mgr, &iamv1alpha1.User{}). @@ -36,6 +36,7 @@ func SetupUserWebhooksWithManager(mgr ctrl.Manager, systemNamespace string, user scheme: mgr.GetScheme(), systemNamespace: systemNamespace, userSelfManageRoleName: userSelfManageRoleName, + selfAuditLogRoleName: selfAuditLogRoleName, }). Complete() } @@ -48,6 +49,11 @@ type UserValidator struct { decoder admission.Decoder systemNamespace string userSelfManageRoleName string + // selfAuditLogRoleName grants self-scoped audit-log query access. The role is + // shipped by the activity service overlay; when activity is not deployed this + // is empty and the binding is skipped, keeping the core control plane free of + // activity coupling. + selfAuditLogRoleName string } func (v *UserValidator) ValidateCreate(ctx context.Context, user *iamv1alpha1.User) (admission.Warnings, error) { @@ -67,6 +73,11 @@ func (v *UserValidator) ValidateCreate(ctx context.Context, user *iamv1alpha1.Us return nil, fmt.Errorf("failed to create owner policy binding: %w", err) } + if err := v.createSelfAuditLogPolicyBinding(ctx, user); err != nil { + userlog.Error(err, "Failed to create self audit-log policy binding") + return nil, fmt.Errorf("failed to create self audit-log policy binding: %w", err) + } + userPreferences, err := v.createUserPreference(ctx, user) if err != nil { userlog.Error(err, "Failed to create user preference") @@ -242,6 +253,53 @@ func (v *UserValidator) createSelfManagePolicyBinding(ctx context.Context, user return nil } +// createSelfAuditLogPolicyBinding grants the user self-scoped audit-log query +// access. The role lives in the activity service overlay; when activity is not +// deployed, selfAuditLogRoleName is empty and this is skipped, so the core +// control plane carries zero activity coupling. The binding is scoped to the +// user's own User resource, mirroring the self-manage binding. +func (v *UserValidator) createSelfAuditLogPolicyBinding(ctx context.Context, user *iamv1alpha1.User) error { + if v.selfAuditLogRoleName == "" { + return nil + } + + userlog.Info("Attempting to create self audit-log PolicyBinding for new user", "user", user.Name) + + policyBinding := &iamv1alpha1.PolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("user-self-audit-log-%s", user.Name), + Namespace: v.systemNamespace, + }, + Spec: iamv1alpha1.PolicyBindingSpec{ + RoleRef: iamv1alpha1.RoleReference{ + Name: v.selfAuditLogRoleName, + Namespace: v.systemNamespace, + }, + Subjects: []iamv1alpha1.Subject{ + { + Kind: "User", + Name: user.Name, + UID: string(user.GetUID()), + }, + }, + ResourceSelector: iamv1alpha1.ResourceSelector{ + ResourceRef: &iamv1alpha1.ResourceReference{ + APIGroup: iamv1alpha1.SchemeGroupVersion.Group, + Kind: "User", + Name: user.Name, + UID: string(user.GetUID()), + }, + }, + }, + } + + if err := v.client.Create(ctx, policyBinding); err != nil { + return fmt.Errorf("failed to create self audit-log policy binding resource: %w", err) + } + + return nil +} + // createUserPreference creates a UserPreference for the new user func (v *UserValidator) createUserPreference(ctx context.Context, user *iamv1alpha1.User) (*iamv1alpha1.UserPreference, error) { userlog.Info("Attempting to create UserPreference for new user", "user", user.Name)