Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/milo/controller-manager/controllermanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions config/services/activity/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component

resources:
- roles/activity-self-audit-log-querier.yaml

components:
- policies
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions internal/controllers/iam/user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
60 changes: 59 additions & 1 deletion internal/webhooks/iam/v1alpha1/user_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}).
Expand All @@ -36,6 +36,7 @@ func SetupUserWebhooksWithManager(mgr ctrl.Manager, systemNamespace string, user
scheme: mgr.GetScheme(),
systemNamespace: systemNamespace,
userSelfManageRoleName: userSelfManageRoleName,
selfAuditLogRoleName: selfAuditLogRoleName,
}).
Complete()
}
Expand All @@ -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) {
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
Loading