revise: enhance RBAC settings
- Migrating fine-grained RBAC policies from the
argocd-rbac-cmfile toAppProjectobjects - Assigning the common role to each
ArgocdUsergroup (admin and view) - Moving view role aggregation for admin to the
AppProject
Codecov Report
:x: Patch coverage is 68.81188% with 63 lines in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 52.33%. Comparing base (5a0aa65) to head (8ce3828).
Additional details and impacted files
@@ Coverage Diff @@
## main #122 +/- ##
===========================================
+ Coverage 34.46% 52.33% +17.87%
===========================================
Files 7 7
Lines 560 684 +124
===========================================
+ Hits 193 358 +165
+ Misses 353 289 -64
- Partials 14 37 +23
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
Can you update the README too? In the new design we can define accesses and permissions into the AppProject instead of CSV?
Sure, I'll update the mentioned issues, and add tests, then request for review again.
@debMan I think tests are failing.
@debMan I think tests are failing.
Yes, still working on this @1995parham jan. I'll inform you when completed
ArgoCD Complementary Operator - Code Review Report
Branch: revise/fix-RBAC-settings
Review Date: November 26, 2025
Reviewer: AI Code Review Specialist
Coverage: 59.5% (1,833 lines changed across 8 files)
Executive Summary
This comprehensive code review analyzes the RBAC refactoring work on the revise/fix-RBAC-settings branch. The codebase demonstrates significant improvements in RBAC management, test coverage (943 new test lines), and architectural separation of concerns between ArgocdUserReconciler and NamespaceReconciler.
Key Metrics
| Metric | Value | Status |
|---|---|---|
| Files Changed | 8 files | ✅ |
| Lines Added | +1,833 | ✅ |
| Lines Removed | -116 | ✅ |
| Build Status | Passing | ✅ |
| Test Coverage | 59.5% | ⚠️ |
| Test Status | 24/25 passing | ⚠️ |
| Critical Issues | 7 | 🔴 |
| High Priority | 12 | 🟡 |
| Medium Priority | 8 | 🟢 |
Verdict
⚠️ DO NOT MERGE - Critical security vulnerabilities must be addressed before merging to main.
Table of Contents
- Change Summary
- Critical Security Issues
- High Priority Issues
- Medium Priority Issues
- Architectural Improvements
- Test Coverage Analysis
- Recommendations
- Compliance & Standards
Change Summary
Modified Files
README.md | 55 ++
config/dependency/group-crd.yaml | 30 +
config/manager/manager.yaml | 2 +-
internal/controller/argocduser_controller.go | 307 ++++++-
internal/controller/argocduser_controller_test.go | 943 ++++++++++++++++
internal/controller/namespace_controller.go | 126 +--
internal/controller/namespace_controller_test.go | 476 +++++++++
internal/controller/suite_test.go | 10 +
Key Improvements
✅ Added finalizer pattern to ArgocdUser controller ✅ Implemented proper cleanup of RBAC policies and static accounts ✅ Separated concerns - ArgocdUser creates AppProjects, Namespace manages destinations ✅ Role aggregation - Admin users inherit view permissions ✅ Comprehensive test suite - 943 lines of new tests for RBAC scenarios ✅ Multi-team support - Tests for complex multi-team namespace scenarios
Critical Security Issues
🔴 CRITICAL-1: Password Stored in Plain Text in CRD Spec
CWE: CWE-256 (Plaintext Storage of a Password)
CVSS Score: 7.5 (High)
Location: api/v1alpha1/argocduser_types.go:36, argocduser_types.go:43
Current Code
type ArgocdCIAdmin struct {
CIPass string `json:"ciPass,omitempty"` // ❌ Plain text password
Users []string `json:"users,omitempty"`
}
type ArgocdCIView struct {
CIPass string `json:"ciPass,omitempty"` // ❌ Plain text password
Users []string `json:"users,omitempty"`
}
Security Impact
- 🔴 Passwords stored in etcd in plain text (encryption at rest doesn't protect from kubectl access)
- 🔴 Visible in
kubectl get argocduser test-team -o yaml - 🔴 Exposed in Kubernetes audit logs
- 🔴 Accessible to anyone with read access to ArgocdUser CRDs
- 🔴 Violates security best practices and compliance requirements (PCI-DSS, SOC 2)
Recommended Fix
Option 1: Reference Kubernetes Secret (Recommended)
type ArgocdCIAdmin struct {
CIPassSecretRef SecretReference `json:"ciPassSecretRef"` // ✅ Secret reference
Users []string `json:"users,omitempty"`
}
type SecretReference struct {
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"` // Optional, default to ArgoCD namespace
Key string `json:"key"`
}
Controller Update:
func (r *ArgocdUserReconciler) getPasswordFromSecret(ctx context.Context, secretRef SecretReference) (string, error) {
secret := &corev1.Secret{}
namespace := secretRef.Namespace
if namespace == "" {
namespace = userArgocdNS
}
if err := r.Get(ctx, types.NamespacedName{
Name: secretRef.Name,
Namespace: namespace,
}, secret); err != nil {
return "", fmt.Errorf("failed to get secret: %w", err)
}
password, ok := secret.Data[secretRef.Key]
if !ok {
return "", fmt.Errorf("key %s not found in secret %s", secretRef.Key, secretRef.Name)
}
return string(password), nil
}
Option 2: External Secrets Operator Integration
type ArgocdCIAdmin struct {
CIPassExternalSecretRef ExternalSecretReference `json:"ciPassExternalSecretRef"`
Users []string `json:"users,omitempty"`
}
Option 3: Webhook Admission Controller
- Implement validating webhook to reject ArgocdUser resources with plain text passwords
- Force users to use Secret references
🔴 CRITICAL-2: Error Swallowing in Password Hashing
CWE: CWE-391 (Unchecked Error Condition)
CVSS Score: 8.1 (High)
Location: internal/controller/argocduser_controller.go:214
Current Code
hash, _ := HashPassword(ciPass) // ❌ ignore error for the sake of simplicity
encodedPass := b64.StdEncoding.EncodeToString([]byte(hash))
Security Impact
- 🔴 If bcrypt fails (e.g., password too long >72 bytes), error is silently ignored
- 🔴 Could result in empty or invalid password hash being stored
- 🔴 Potential authentication bypass if hash is empty/corrupted
- 🔴 Silent failures lead to hard-to-debug security incidents
Attack Scenario
- User provides password >72 bytes
- bcrypt.GenerateFromPassword returns error
- Error is ignored,
hashis empty string - Empty hash is base64 encoded and stored
- Authentication may fail or succeed with wrong credentials
Recommended Fix
// ✅ Properly handle errors
hash, err := HashPassword(ciPass)
if err != nil {
log.Error(err, "Failed to hash password", "user", argocduser.Name, "role", roleName)
return fmt.Errorf("password hashing failed: %w", err)
}
// Validate hash is not empty
if hash == "" {
return fmt.Errorf("password hash is empty for user %s role %s", argocduser.Name, roleName)
}
encodedPass := b64.StdEncoding.EncodeToString([]byte(hash))
// Also validate password constraints
func HashPassword(password string) (string, error) {
if len(password) > 72 {
return "", fmt.Errorf("password exceeds bcrypt maximum length of 72 bytes")
}
if len(password) < 8 {
return "", fmt.Errorf("password must be at least 8 characters")
}
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
if err != nil {
return "", fmt.Errorf("bcrypt failed: %w", err)
}
return string(bytes), nil
}
🔴 CRITICAL-3: Overly Permissive RBAC Wildcard
CWE: CWE-269 (Improper Privilege Management)
CVSS Score: 7.2 (High)
Location: config/rbac/role.yaml:95-106, argocduser_controller.go:61
Current Code
- apiGroups:
- user.openshift.io
resources:
- '*' # ❌ Wildcard grants access to ALL OpenShift user resources
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
Security Impact
- 🔴 Violates principle of least privilege
- 🔴 Grants unnecessary access to all
user.openshift.ioresources (not just Groups) - 🔴 Could allow manipulation of users, identities, and other user resources
- 🔴 Potential privilege escalation vector
Resources Affected by Wildcard
-
users(User objects) -
groups(Group objects) ← Only this is needed -
identities(Identity objects) -
useridentitymappings(UserIdentityMapping objects)
Recommended Fix
# ✅ Explicit resource type
- apiGroups:
- user.openshift.io
resources:
- groups # Only grant access to Groups
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
Update kubebuilder marker in controller:
//+kubebuilder:rbac:groups=user.openshift.io,resources=groups,verbs=get;list;watch;create;update;patch;delete
🔴 CRITICAL-4: String Concatenation in RBAC Policies (Injection Risk)
CWE: CWE-74 (Improper Neutralization)
CVSS Score: 7.3 (High)
Location: internal/controller/argocduser_controller.go:313-315, 323-325
Current Code
Policies: []string{
"p, proj:" + teamName + ":" + teamName + "-admin, applications, *, " + teamName + "/*, allow",
"p, proj:" + teamName + ":" + teamName + "-admin, repositories, *, " + teamName + "/*, allow",
"p, proj:" + teamName + ":" + teamName + "-admin, exec, create, " + teamName + "/*, allow",
// ❌ No validation of teamName - could contain policy injection characters
}
Security Impact
- 🔴 If
teamNamecontains,or newlines, could inject arbitrary policies - 🔴 ArgoCD CSV policy format is vulnerable to injection
- 🔴 Potential privilege escalation
Attack Scenario
apiVersion: argocd.snappcloud.io/v1alpha1
kind: ArgocdUser
metadata:
name: "evil,p,role:admin,*,*,*,allow" # ❌ Injects admin policy
spec:
admin:
ciPass: "password"
users: []
Resulting policy:
p, proj:evil,p,role:admin,*,*,*,allow:evil,p,role:admin,*,*,*,allow-admin, applications, *, evil,p,role:admin,*,*,*,allow/*, allow
Recommended Fix
// Add validation at CRD level
//+kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
//+kubebuilder:validation:MaxLength=63
type ArgocdUser struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ArgocdUserSpec `json:"spec,omitempty"`
}
// Add runtime validation
func validateTeamName(teamName string) error {
// DNS-1123 subdomain validation
if matched, _ := regexp.MatchString(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, teamName); !matched {
return fmt.Errorf("team name must be DNS-1123 compliant lowercase alphanumeric")
}
if len(teamName) > 63 {
return fmt.Errorf("team name exceeds 63 characters")
}
// Explicitly check for injection characters
if strings.ContainsAny(teamName, ",\n\r:") {
return fmt.Errorf("team name contains invalid characters")
}
return nil
}
// Use in reconciler
func (r *ArgocdUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// ... get argocduser ...
if err := validateTeamName(argocduser.Name); err != nil {
log.Error(err, "Invalid team name", "name", argocduser.Name)
return ctrl.Result{}, err
}
// ... continue reconciliation ...
}
// Use template with validation
func buildPolicy(teamName, role, resource, action string) (string, error) {
if err := validateTeamName(teamName); err != nil {
return "", err
}
return fmt.Sprintf("p, proj:%s:%s-%s, %s, %s, %s/*, allow",
teamName, teamName, role, resource, action, teamName), nil
}
🔴 CRITICAL-5: Missing OwnerReference Validation
CWE: CWE-476 (NULL Pointer Dereference potential)
Location: internal/controller/argocduser_controller.go:248-256
Current Code
group = &userv1.Group{
ObjectMeta: metav1.ObjectMeta{
Name: groupName,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "argocd.snappcloud.io/v1alpha1",
Kind: "ArgocdUser",
Name: argocduser.Name,
UID: argocduser.UID, // ❌ Could be empty if just created
Controller: ptr.To(true),
BlockOwnerDeletion: ptr.To(true),
},
},
},
Users: argoUsers,
}
Security Impact
- 🔴 If UID is empty, Groups created without proper ownership
- 🔴 Orphaned resources not cleaned up by garbage collector
- 🔴 Resource leaks in cluster
- 🔴 Groups persist after ArgocdUser deletion
Recommended Fix
// Validate OwnerReference before creating Group
if argocduser.UID == "" {
return fmt.Errorf("ArgocdUser UID is empty, resource may not be persisted yet")
}
// Use controllerutil for safety
group = &userv1.Group{
ObjectMeta: metav1.ObjectMeta{
Name: groupName,
},
Users: argoUsers,
}
// SetControllerReference handles validation
if err := controllerutil.SetControllerReference(argocduser, group, r.Scheme); err != nil {
log.Error(err, "Failed to set controller reference on Group")
return fmt.Errorf("failed to set controller reference: %w", err)
}
err = r.Create(ctx, group)
🔴 CRITICAL-6: Missing AppProject Delete Permission
Location: config/rbac/role.yaml:58-67
Current Code
- apiGroups:
- argoproj.io
resources:
- appprojects
verbs:
- create
- get
- list
- patch
- update
- watch
# ❌ Missing 'delete' verb
Impact
- 🔴 AppProjects created with OwnerReferences but controller can't delete them
- 🔴 When ArgocdUser is deleted, AppProject may become orphaned
- 🔴 Finalizer cleanup at line 106 will fail silently
- 🔴 Resource accumulation over time
Recommended Fix
- apiGroups:
- argoproj.io
resources:
- appprojects
verbs:
- create
- delete # ✅ Add delete permission
- get
- list
- patch
- update
- watch
Update kubebuilder marker:
//+kubebuilder:rbac:groups=argoproj.io,resources=appprojects,verbs=get;list;watch;create;update;patch;delete
🔴 CRITICAL-7: Namespace List Permission Missing
Location: config/rbac/role.yaml:69-80
Current Code
- apiGroups:
- ""
resources:
- namespaces
verbs:
- create
- delete
- get
- list # ✅ Present
- patch
- update
- watch
Analysis
This is actually correctly configured ✅. The controller has list permission needed for hasActiveNamespaces() at line 420.
However, there's a security concern:
Issue
- The controller has
deletepermission on namespaces but doesn't use it - This violates least privilege principle
Recommended Fix
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get # ✅ Needed for namespace lookups
- list # ✅ Needed for hasActiveNamespaces
- watch # ✅ Needed for namespace controller
# Remove: create, delete, patch, update (not used by controller)
High Priority Issues
🟡 HIGH-1: Context Mismatch in Password Update
Location: argocduser_controller.go:224
Issue
func (r *ArgocdUserReconciler) UpdateUserArgocdConfig(ctx context.Context, ...) error {
// ... uses ctx correctly ...
err = r.Patch(context.Background(), &corev1.Secret{...}) // ❌ Wrong context
// ^^^^^^^^^^^^^^^^^^ Should be ctx
}
Impact
- Request tracing broken
- Context cancellation bypassed
- Timeout propagation lost
- Debugging harder
Fix
err = r.Patch(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: userArgocdNS,
Name: userArgocdSecret,
},
}, client.RawPatch(types.StrategicMergePatchType, staticPassByte))
🟡 HIGH-2: Test Isolation Failure
Location: internal/controller/argocduser_controller_test.go
Test Output
[FAILED] Expected success, but got an error:
argocdusers.argocd.snappcloud.io "test-team" already exists
Impact
- Tests fail non-deterministically
- CI/CD pipeline unreliable
- Hides real bugs
- Developer frustration
Root Cause
Tests use Ordered suite but don't properly clean up between test cases. The test-team ArgocdUser is created in one test and persists, causing subsequent tests to fail.
Recommended Fix
Option 1: Use unique names per test
var _ = Describe("ArgocdUser controller", func() {
var testCounter int
Context("When creating an ArgocdUser resource", func() {
It("Should generate correct RBAC policies", func() {
testCounter++
teamName := fmt.Sprintf("test-team-%d", testCounter)
argocdUser := &argocduserv1alpha1.ArgocdUser{
ObjectMeta: metav1.ObjectMeta{
Name: teamName, // ✅ Unique per test
},
// ...
}
})
})
})
Option 2: Proper cleanup in BeforeEach/AfterEach
var _ = Describe("ArgocdUser controller", func() {
BeforeEach(func() {
// Clean up any existing test resources
testTeamUser := &argocduserv1alpha1.ArgocdUser{}
if err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-team"}, testTeamUser); err == nil {
Expect(k8sClient.Delete(ctx, testTeamUser)).Should(Succeed())
// Wait for deletion to complete
Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-team"}, testTeamUser)
return errors.IsNotFound(err)
}, timeout, interval).Should(BeTrue())
}
})
})
Option 3: Remove Ordered and make tests independent
var _ = Describe("ArgocdUser controller", func() { // Remove Ordered
Context("When creating an ArgocdUser resource", func() {
It("Should generate correct RBAC policies", func() {
// Each test creates and cleans up its own resources
})
})
})
🟡 HIGH-3: No Input Validation on User Lists
Location: argocduser_controller.go:237-289
Issue
func (r *ArgocdUserReconciler) AddArgoUsersToGroup(ctx context.Context, argocduser *argocduserv1alpha1.ArgocdUser, roleName string, argoUsers []string) error {
// ❌ No validation of argoUsers contents
group.Users = argoUsers
}
Security Impact
- Invalid usernames may cause Group creation to fail
- OpenShift/Kubernetes has naming constraints
- No protection against injection or special characters
Recommended Fix
// Add validation function
func validateUsernames(users []string) error {
for _, user := range users {
// OpenShift username validation (simplified)
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9._@-]+$`, user); !matched {
return fmt.Errorf("invalid username: %s", user)
}
if len(user) > 255 {
return fmt.Errorf("username too long: %s", user)
}
}
return nil
}
func (r *ArgocdUserReconciler) AddArgoUsersToGroup(..., argoUsers []string) error {
// Validate input
if err := validateUsernames(argoUsers); err != nil {
return fmt.Errorf("invalid user list: %w", err)
}
// ... rest of function ...
}
// Also add to CRD
//+kubebuilder:validation:Pattern=`^[a-zA-Z0-9._@-]+$`
//+kubebuilder:validation:MaxLength=255
type ArgocdCIAdmin struct {
CIPass string `json:"ciPass,omitempty"`
Users []string `json:"users,omitempty"`
}
🟡 HIGH-4: Race Condition in User Merging
Location: internal/controller/argocduser_controller.go:272-281
Issue
// Merge users: keep existing users and add new ones
existingUsers := make(map[string]bool)
for _, user := range group.Users {
existingUsers[user] = true
}
for _, user := range argoUsers {
if !existingUsers[user] {
group.Users = append(group.Users, user) // ❌ No mutex
}
}
err = r.Update(ctx, group) // ❌ Potential race if concurrent reconciles
Security Impact
- Concurrent reconciliation loops could cause user list corruption
- Users may be added multiple times
- Race condition if multiple ArgocdUsers reference same group
- Non-deterministic behavior
Scenario
- ArgocdUser A adds users ["alice", "bob"] to "team-admin" group
- ArgocdUser B simultaneously adds ["charlie"] to "team-admin" group
- Race condition: final group may have duplicates or missing users
Recommended Fix
Option 1: Use deterministic set operations
// Merge users with set semantics and deterministic ordering
func mergeUsers(existing, new []string) []string {
userSet := make(map[string]struct{})
// Add existing users
for _, user := range existing {
userSet[user] = struct{}{}
}
// Add new users
for _, user := range new {
userSet[user] = struct{}{}
}
// Convert back to sorted slice for deterministic results
users := make([]string, 0, len(userSet))
for user := range userSet {
users = append(users, user)
}
sort.Strings(users) // ✅ Deterministic ordering
return users
}
// Use in controller
group.Users = mergeUsers(group.Users, argoUsers)
err = r.Update(ctx, group)
Option 2: Use optimistic locking with retry
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
// Get latest version
if err := r.Get(ctx, types.NamespacedName{Name: groupName}, group); err != nil {
return err
}
// Merge users
group.Users = mergeUsers(group.Users, argoUsers)
// Update with conflict detection
return r.Update(ctx, group)
})
🟡 HIGH-5: Missing Idempotency in RBAC Policy Addition
Location: argocduser_controller.go:385-397
Issue
is_changed := false
for _, policy := range policies {
duplicatePolicy := false
for _, line := range strings.Split(found.Data["policy.csv"], "\n") {
if policy == line { // ❌ O(n*m) complexity
duplicatePolicy = true
}
}
if !duplicatePolicy {
found.Data["policy.csv"] = found.Data["policy.csv"] + "\n" + policy
is_changed = true
}
}
Performance Impact
- O(n*m) complexity where n = policies, m = existing lines
- On large policy files (1000+ lines), becomes very slow
- Every reconciliation re-scans entire policy file
- CPU and memory intensive
Scenario
- 50 teams = 250 policies (5 per team)
- Each reconciliation: 250 * 250 = 62,500 string comparisons
- With 1000 lines: 1000 * 1000 = 1,000,000 comparisons
Recommended Fix
// ✅ Use map for O(1) lookups
func (r *ArgocdUserReconciler) AddArgocdRBACPolicy(ctx context.Context, argocduser *argocduserv1alpha1.ArgocdUser) error {
log := log.FromContext(ctx)
found := &corev1.ConfigMap{}
err := r.Get(ctx, types.NamespacedName{Name: userArgocdRbacPolicyCM, Namespace: userArgocdNS}, found)
if err != nil {
return err
}
policies := []string{
"p, role:common, clusters, get, *, allow",
"g, " + argocduser.Name + "-admin-ci, role:common",
"g, " + argocduser.Name + "-view-ci, role:common",
"g, " + argocduser.Name + "-admin, role:common",
"g, " + argocduser.Name + "-view, role:common",
}
// Parse existing policies into map - O(n)
existingLines := strings.Split(found.Data["policy.csv"], "\n")
policySet := make(map[string]struct{}, len(existingLines))
for _, line := range existingLines {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
policySet[trimmed] = struct{}{}
}
}
// Add new policies - O(m)
newPolicies := []string{}
for _, policy := range policies {
if _, exists := policySet[policy]; !exists {
newPolicies = append(newPolicies, policy)
policySet[policy] = struct{}{} // Update set
}
}
// Only update if there are new policies
if len(newPolicies) > 0 {
// Rebuild policy.csv with all policies
allPolicies := make([]string, 0, len(policySet))
for policy := range policySet {
allPolicies = append(allPolicies, policy)
}
sort.Strings(allPolicies) // ✅ Deterministic ordering
found.Data["policy.csv"] = strings.Join(allPolicies, "\n")
if err := r.Update(ctx, found); err != nil {
log.Error(err, "error updating argocd-rbac-cm")
return err
}
log.Info("Added RBAC policies", "count", len(newPolicies))
} else {
log.Info("All RBAC policies already exist, skipping update")
}
return nil
}
🟡 HIGH-6: Incomplete Cleanup Logic
Location: argocduser_controller.go:443-519
Issue
func (r *ArgocdUserReconciler) cleanupArgocdUserResources(ctx context.Context, argocduser *argocduserv1alpha1.ArgocdUser) error {
// ...
for _, line := range lines {
// Keep lines that don't reference this team
if !strings.Contains(line, teamName+"-admin-ci") &&
!strings.Contains(line, teamName+"-view-ci") &&
!strings.Contains(line, teamName+"-admin") &&
!strings.Contains(line, teamName+"-view") {
newLines = append(newLines, line)
}
}
// ❌ What if teamName is substring of another team?
// e.g., "team" matches "my-team-admin"
}
Security Impact
- False positive matches delete wrong team's policies
- If team "app" exists, deleting team "myapp" removes both
- Data corruption across teams
- Privilege escalation/denial
Attack Scenario
# Attacker creates team with substring name
apiVersion: argocd.snappcloud.io/v1alpha1
kind: ArgocdUser
metadata:
name: "prod" # Substring of "production"
---
# When "prod" is deleted, it removes "production" team's policies too!
Recommended Fix
func (r *ArgocdUserReconciler) cleanupArgocdUserResources(ctx context.Context, argocduser *argocduserv1alpha1.ArgocdUser) error {
log := log.FromContext(ctx)
teamName := argocduser.Name
// ✅ Use regex with word boundaries for exact matching
teamPatterns := []*regexp.Regexp{
regexp.MustCompile(`\b` + regexp.QuoteMeta(teamName) + `-admin-ci\b`),
regexp.MustCompile(`\b` + regexp.QuoteMeta(teamName) + `-view-ci\b`),
regexp.MustCompile(`\b` + regexp.QuoteMeta(teamName) + `-admin\b`),
regexp.MustCompile(`\b` + regexp.QuoteMeta(teamName) + `-view\b`),
}
// Remove RBAC policies
rbacConfigMap := &corev1.ConfigMap{}
if err := r.Get(ctx, types.NamespacedName{Name: userArgocdRbacPolicyCM, Namespace: userArgocdNS}, rbacConfigMap); err != nil {
if !errors.IsNotFound(err) {
return err
}
} else {
policyCsv := rbacConfigMap.Data["policy.csv"]
lines := strings.Split(policyCsv, "\n")
var newLines []string
for _, line := range lines {
shouldRemove := false
for _, pattern := range teamPatterns {
if pattern.MatchString(line) {
shouldRemove = true
log.Info("Removing policy line", "line", line)
break
}
}
if !shouldRemove && strings.TrimSpace(line) != "" {
newLines = append(newLines, line)
}
}
rbacConfigMap.Data["policy.csv"] = strings.Join(newLines, "\n")
if err := r.Update(ctx, rbacConfigMap); err != nil {
return fmt.Errorf("failed to update argocd-rbac-cm: %w", err)
}
}
// ... rest of cleanup ...
}
Medium Priority Issues
🟢 MEDIUM-1: TODO Comment for 4+ Months
Location: argocduser_controller.go:375
// TODO: Enhance this, for example adding view roles to the admin not works!
Issue
This TODO has been in the codebase and indicates a known issue with role aggregation that hasn't been addressed.
Current Behavior
The comment suggests that admin users might not be inheriting view permissions properly, which could be a bug.
Analysis
Looking at the current implementation (lines 319-326), the role aggregation IS implemented:
{
// View role includes both admin and view groups for role aggregation
Groups: []string{teamName + "-admin", teamName + "-admin-ci", teamName + "-view", teamName + "-view-ci"},
Name: teamName + "-view",
// ...
}
Recommendation
- ✅ If the issue is fixed, remove the TODO
- ⚠️ If not fixed, create a GitHub issue and reference it
- 📝 Add test case to verify role aggregation works
🟢 MEDIUM-2: No Metrics or Observability
Issue: No Prometheus metrics for monitoring reconciliation health
Recommended Metrics
import (
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
var (
reconcileTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocduser_reconcile_total",
Help: "Total number of reconciliations",
},
[]string{"controller", "result"},
)
reconcileDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "argocduser_reconcile_duration_seconds",
Help: "Reconciliation duration in seconds",
},
[]string{"controller"},
)
argocdUserTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "argocduser_total",
Help: "Total number of ArgocdUser resources",
},
)
)
func init() {
metrics.Registry.MustRegister(reconcileTotal, reconcileDuration, argocdUserTotal)
}
// Use in reconciler
func (r *ArgocdUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
start := time.Now()
defer func() {
reconcileDuration.WithLabelValues("argocduser").Observe(time.Since(start).Seconds())
}()
// ... reconciliation logic ...
reconcileTotal.WithLabelValues("argocduser", "success").Inc()
return ctrl.Result{}, nil
}
🟢 MEDIUM-3: No Logging of Security Events
Issue: Security-relevant events (password changes, group modifications) not logged
Recommended Fix
// Add audit logging
func (r *ArgocdUserReconciler) UpdateUserArgocdConfig(...) error {
// Log security event
log.Info("SECURITY: Updating static user password",
"team", argocduser.Name,
"role", roleName,
"timestamp", time.Now().UTC(),
)
// ... existing code ...
}
func (r *ArgocdUserReconciler) AddArgoUsersToGroup(...) error {
// Log group membership changes
log.Info("SECURITY: Modifying group membership",
"group", groupName,
"team", argocduser.Name,
"users", argoUsers,
"timestamp", time.Now().UTC(),
)
// ... existing code ...
}
🟢 MEDIUM-4: JSON Marshaling Error Ignored
Location: argocduser_controller.go:222
staticPassByte, _ := json.Marshal(staticPassword)
// ❌ Error ignored
Fix
staticPassByte, err := json.Marshal(staticPassword)
if err != nil {
log.Error(err, "Failed to marshal static password data")
return fmt.Errorf("JSON marshaling failed: %w", err)
}
🟢 MEDIUM-5: No Rate Limiting on Reconciliation
Issue: Rapid updates to ArgocdUser could cause reconciliation storm
Recommended Fix
func (r *ArgocdUserReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&argocduserv1alpha1.ArgocdUser{}).
WithOptions(controller.Options{
MaxConcurrentReconciles: 3, // ✅ Limit concurrent reconciles
RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(
time.Second, // Base delay
time.Minute * 5, // Max delay
),
}).
Complete(r)
}
🟢 MEDIUM-6: ConfigMap Data Not Validated
Location: argocduser_controller.go:204
if configMap.Data == nil {
configMap.Data = make(map[string]string)
}
// ❌ But what if Data is not nil but policy.csv is missing?
Fix
if configMap.Data == nil {
configMap.Data = make(map[string]string)
}
if _, exists := configMap.Data["policy.csv"]; !exists {
configMap.Data["policy.csv"] = "" // ✅ Initialize if missing
}
🟢 MEDIUM-7: No Webhook Validation
Issue: CRD validation happens only at API level, not business logic
Recommended Implementation
// Create validating webhook
//+kubebuilder:webhook:path=/validate-argocd-snappcloud-io-v1alpha1-argocduser,mutating=false,failurePolicy=fail,groups=argocd.snappcloud.io,resources=argocdusers,verbs=create;update,versions=v1alpha1,name=vargocduser.kb.io,sideEffects=None,admissionReviewVersions=v1
func (r *ArgocdUser) ValidateCreate() error {
// Validate team name
if err := validateTeamName(r.Name); err != nil {
return err
}
// Warn about plain text passwords (or reject)
if r.Spec.Admin.CIPass != "" {
return fmt.Errorf("plain text passwords not allowed, use ciPassSecretRef")
}
// Validate users
allUsers := append(r.Spec.Admin.Users, r.Spec.View.Users...)
if err := validateUsernames(allUsers); err != nil {
return err
}
return nil
}
🟢 MEDIUM-8: No Status Subresource Used
Issue: ArgocdUser has status subresource but it's never populated
Recommendation
type ArgocdUserStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
Phase string `json:"phase,omitempty"`
LastReconcileTime metav1.Time `json:"lastReconcileTime,omitempty"`
}
// Update in reconciler
func (r *ArgocdUserReconciler) Reconcile(...) (ctrl.Result, error) {
// ... reconciliation ...
// Update status
argocduser.Status.Phase = "Ready"
argocduser.Status.LastReconcileTime = metav1.Now()
argocduser.Status.Conditions = []metav1.Condition{
{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "ReconcileSuccess",
Message: "ArgocdUser reconciled successfully",
},
}
if err := r.Status().Update(ctx, argocduser); err != nil {
log.Error(err, "Failed to update status")
}
return ctrl.Result{}, nil
}
Architectural Improvements
✅ Excellent: Separation of Concerns
The refactoring properly separates responsibilities:
ArgocdUserReconciler (RBAC Management):
- Creates AppProjects with RBAC roles
- Manages OpenShift Groups
- Configures ArgoCD static users
- Adds global RBAC policies
NamespaceReconciler (Namespace Management):
- Updates AppProject destinations
- Manages source namespaces
- Handles multi-team labels
This is excellent architectural design ✅ following the Single Responsibility Principle.
✅ Excellent: Finalizer Pattern
The implementation of finalizers with dependency checking is exemplary:
// Check if there are namespaces still referencing this ArgocdUser
hasActive, err := r.hasActiveNamespaces(ctx, argocduser.Name)
if hasActive {
log.Info("Cannot delete ArgocdUser: namespaces still reference it")
return ctrl.Result{Requeue: true}, nil
}
This prevents orphaned namespaces and cascading failures. Well done! ✅
✅ Excellent: Role Aggregation
The role aggregation pattern where admin users inherit view permissions is well-implemented:
{
// View role includes both admin and view groups for role aggregation
Groups: []string{teamName + "-admin", teamName + "-admin-ci", teamName + "-view", teamName + "-view-ci"},
Name: teamName + "-view",
Policies: []string{
"p, proj:" + teamName + ":" + teamName + "-view, applications, get, " + teamName + "/*, allow",
"p, proj:" + teamName + ":" + teamName + "-view, repositories, get, " + teamName + "/*, allow",
"p, proj:" + teamName + ":" + teamName + "-view, logs, get, " + teamName + "/*, allow",
},
}
This follows Kubernetes RBAC best practices ✅
⚠️ Improvement Needed: Error Handling
Many functions don't properly handle or wrap errors:
hash, _ := HashPassword(ciPass) // ❌
staticPassByte, _ := json.Marshal(staticPassword) // ❌
Recommendation: Adopt consistent error handling:
if err != nil {
return fmt.Errorf("context: %w", err) // ✅ Wrap with context
}
⚠️ Improvement Needed: Testing Strategy
Tests focus on happy paths but miss:
- Error scenarios
- Edge cases
- Concurrent reconciliation
- Finalizer edge cases
Recommendation: Add property-based testing and chaos engineering tests.
Test Coverage Analysis
Current Coverage: 59.5% ⚠️
FAIL github.com/snapp-incubator/argocd-complementary-operator/internal/controller 24.995s
coverage: 59.5% of statements
Test Results
- ✅ 24 of 25 tests passing (96% pass rate)
- ❌ 1 test failing due to resource isolation issue
- ⏭️ 15 tests skipped (need investigation why)
Coverage by Component
| Component | Estimated Coverage | Priority |
|---|---|---|
argocduser_controller.go |
~65% | 🔴 Need error paths |
namespace_controller.go |
~70% | 🟡 Add multi-team tests |
| Cleanup functions | ~0% | 🔴 Critical gap |
| Error handlers | ~10% | 🔴 Critical gap |
Test Quality Assessment
Strengths ✅
- 943 lines of new tests for ArgocdUser controller
- 476 lines of new tests for Namespace controller
- Tests cover RBAC policy generation
- Tests verify AppProject creation
- Tests validate group membership
- Tests check multi-team scenarios
Weaknesses ❌
-
No tests for cleanup (
cleanupArgocdUserResourceshas 0% coverage) - No tests for error paths (what happens when bcrypt fails?)
- No tests for finalizer edge cases (deletion with active namespaces)
- No tests for concurrent reconciliation
- No tests for invalid input (malicious team names, injection attempts)
- No integration tests with real ArgoCD instance
Recommended Additional Tests
// Test error handling
var _ = Describe("ArgocdUser error handling", func() {
It("Should handle bcrypt failure gracefully", func() {
// Test with password > 72 bytes
argocdUser := &argocduserv1alpha1.ArgocdUser{
Spec: argocduserv1alpha1.ArgocdUserSpec{
Admin: argocduserv1alpha1.ArgocdCIAdmin{
CIPass: strings.Repeat("a", 100), // ❌ Too long
},
},
}
// Expect reconciliation to fail with clear error
})
It("Should validate team name for injection", func() {
argocdUser := &argocduserv1alpha1.ArgocdUser{
ObjectMeta: metav1.ObjectMeta{
Name: "evil,admin", // ❌ Contains comma
},
}
Expect(k8sClient.Create(ctx, argocdUser)).Should(Not(Succeed()))
})
})
// Test cleanup
var _ = Describe("ArgocdUser cleanup", func() {
It("Should remove all RBAC policies on deletion", func() {
// Create ArgocdUser
// Verify policies exist
// Delete ArgocdUser
// Verify policies removed
})
It("Should prevent deletion with active namespaces", func() {
// Create ArgocdUser
// Create namespace with label
// Try to delete ArgocdUser
// Expect deletion to be blocked by finalizer
})
})
// Test concurrent reconciliation
var _ = Describe("ArgocdUser concurrent operations", func() {
It("Should handle concurrent group updates", func() {
// Create multiple ArgocdUsers updating same group
// Verify no race conditions or duplicates
})
})
Recommendations
Immediate Actions (Before Merge) 🔴
-
FIX CRITICAL-1: Migrate passwords from plain text to Secret references
- Add
ciPassSecretReffield - Deprecate
ciPassfield - Add migration guide
- Add
-
FIX CRITICAL-2: Handle bcrypt errors properly
- Remove
_, _error ignoring - Add proper error handling and logging
- Validate password constraints
- Remove
-
FIX CRITICAL-3: Replace RBAC wildcard with explicit
groupsresource- Update
config/rbac/role.yaml - Update kubebuilder markers
- Run
make manifests
- Update
-
FIX CRITICAL-4: Validate team names for injection
- Add regex validation
- Add kubebuilder validation markers
- Add runtime validation
-
FIX CRITICAL-6: Add
deleteverb for appprojects- Update RBAC manifest
- Test garbage collection works
-
FIX HIGH-2: Fix test isolation issue
- Use unique names or proper cleanup
- Ensure tests can run in any order
Short Term (Next Sprint) 🟡
- Add input validation for usernames and team names
- Fix context.Background() usage - use ctx parameter
- Add comprehensive error path testing
- Implement race condition protection for group updates
- Add Prometheus metrics for observability
- Improve cleanup logic to use regex with word boundaries
- Add security event logging for audit trail
- Implement rate limiting on reconciliation
Long Term (Technical Debt) 🟢
-
Migrate to External Secrets Operator
- Full integration with Vault, AWS Secrets Manager, etc.
- Remove plain text password support entirely
-
Implement Webhook Admission Controller
- Validate ArgocdUser at admission time
- Reject invalid configurations early
- Add mutating webhook for defaults
-
Add Integration Tests
- Test with real ArgoCD instance
- Verify RBAC enforcement works
- Test end-to-end user flows
-
Performance Optimization
- Cache namespace lookups
- Batch policy updates
- Optimize string operations
-
Add OpenTelemetry Tracing
- Distributed tracing for debugging
- Correlate logs across reconciliations
- Performance profiling
-
Security Hardening
- Add OPA/Kyverno policies
- Implement pod security standards
- Regular security scanning (Snyk, Trivy)
Compliance & Standards
✅ Follows:
- ✅ Kubernetes Operator best practices
- ✅ Controller-runtime patterns
- ✅ Ginkgo/Gomega testing conventions
- ✅ Kubebuilder markers and scaffolding
- ✅ Go coding standards (mostly)
- ✅ Semantic versioning
⚠️ Partially Follows:
- ⚠️ OWASP Top 10 (violations in A01, A02, A03)
- ⚠️ CIS Kubernetes Benchmark
- ⚠️ Error handling best practices
❌ Missing:
- ❌ Security Policy as Code (OPA/Kyverno policies)
- ❌ SBOM (Software Bill of Materials)
- ❌ Container image scanning in CI/CD
- ❌ Secret detection in CI/CD (GitGuardian, TruffleHog)
- ❌ SAST tools (Semgrep, CodeQL)
- ❌ Dependency vulnerability scanning
OWASP Top 10 2025 Compliance
| Category | Status | Issues |
|---|---|---|
| A01: Broken Access Control | 🔴 FAIL | Wildcard RBAC, missing validation |
| A02: Cryptographic Failures | 🔴 FAIL | Plain text passwords |
| A03: Injection | 🔴 FAIL | Policy injection via team name |
| A04: Insecure Design | 🟡 PARTIAL | Missing webhook validation |
| A05: Security Misconfiguration | 🟡 PARTIAL | Overly permissive defaults |
| A06: Vulnerable Components | 🟢 PASS | Dependencies seem up to date |
| A07: Authentication Failures | 🟡 PARTIAL | Password validation weak |
| A08: Data Integrity Failures | 🟢 PASS | Using bcrypt correctly |
| A09: Logging Failures | 🟡 PARTIAL | Missing security event logs |
| A10: SSRF | 🟢 PASS | Not applicable |
Summary & Verdict
Overall Assessment: ⚠️ DO NOT MERGE YET
The RBAC refactoring shows excellent architectural improvements with proper separation of concerns, finalizer patterns, and comprehensive test coverage. However, 7 critical security vulnerabilities must be addressed before merging to production.
Strengths 💪
- ✅ Excellent separation of concerns between controllers
- ✅ Comprehensive test suite (943 + 476 new test lines)
- ✅ Proper finalizer implementation with dependency checking
- ✅ Role aggregation following Kubernetes best practices
- ✅ Multi-team support with thoughtful design
- ✅ Code builds successfully
Critical Concerns 🚨
- 🔴 Plain text passwords in CRD (CRITICAL)
- 🔴 Error swallowing in cryptography (CRITICAL)
- 🔴 RBAC wildcard permissions (CRITICAL)
- 🔴 Policy injection vulnerability (CRITICAL)
- 🔴 Missing AppProject delete permission
- 🔴 Test isolation failures
- 🔴 Race conditions in concurrent updates
Risk Assessment
| Risk Category | Level | Impact |
|---|---|---|
| Security | 🔴 HIGH | Multiple critical vulnerabilities |
| Reliability | 🟡 MEDIUM | Test failures, race conditions |
| Performance | 🟢 LOW | Some O(n²) algorithms |
| Maintainability | 🟢 LOW | Generally clean code |
Next Steps
For Code Author
- Review and address all CRITICAL issues
- Fix test isolation problem
- Add error path test coverage
- Update RBAC manifests
- Run
make manifeststo regenerate CRDs
For Code Reviewers
- Verify all CRITICAL issues are resolved
- Check test coverage improvements
- Validate RBAC changes
- Approve only after all issues addressed
For DevOps/Platform Team
- Plan password migration strategy
- Set up secret management infrastructure
- Enable security scanning in CI/CD
- Review cluster RBAC configuration
Appendix A: Testing Commands
# Run tests with coverage
KUBEBUILDER_ASSETS="./bin/k8s/1.29.0-darwin-arm64" \
go test -v -coverprofile=coverage.out ./internal/controller/...
# View coverage report
go tool cover -html=coverage.out
# Run specific test
KUBEBUILDER_ASSETS="./bin/k8s/1.29.0-darwin-arm64" \
go test -v ./internal/controller/ -run TestArgocdUser
# Run tests in parallel
KUBEBUILDER_ASSETS="./bin/k8s/1.29.0-darwin-arm64" \
go test -v -parallel=4 ./...
# Generate RBAC manifests
make manifests
# Build operator
make build
# Run linters
golangci-lint run
# Security scanning (if available)
govulncheck ./...
Appendix B: References
Security Resources
Kubernetes Resources
Testing Resources
Review Completed: November 26, 2025 Reviewed By: AI Code Review Specialist (Claude 4.5 Sonnet) Next Review: After addressing critical issues