Skip to content

Feature: Add org‑unit scoped custom role management with full CRUD an…#103

Open
Suryanshu185 wants to merge 1 commit into
go-core-stack:mainfrom
Suryanshu185:main
Open

Feature: Add org‑unit scoped custom role management with full CRUD an…#103
Suryanshu185 wants to merge 1 commit into
go-core-stack:mainfrom
Suryanshu185:main

Conversation

@Suryanshu185

@Suryanshu185 Suryanshu185 commented Aug 12, 2025

Copy link
Copy Markdown
Contributor

…d permission checks

Introduce complete support for custom roles scoped to an organization unit in the auth‑gateway. Includes the ability to create, update, retrieve, delete, and list roles within their org‑unit boundaries.

Summary by CodeRabbit

  • New Features

    • Full CRUD for custom Org Unit roles via new APIs; role listings now include type, display name, creator and timestamps and show built-in + custom roles.
    • Gateway applies custom-role permission rules and filters/paginates list responses based on resource-level RBAC/PBAC.
    • Org Unit user create/update validate custom roles; deletions perform binding-aware soft-delete with automatic cleanup of orphaned custom roles.
  • Documentation

    • API docs updated with new endpoints, request/response schemas and permission model entries.
  • Chores

    • Startup now initializes custom role storage.

@coderabbitai

coderabbitai Bot commented Sep 24, 2025

Copy link
Copy Markdown

Walkthrough

Adds org-unit scoped custom-role management: protobuf RPCs and HTTP routes; OpenAPI schemas; gateway route metadata, response buffering/filtering and PBAC checks; server-side CRUD handlers, validation, and user-role cleanup; a Mongo-backed OrgUnitCustomRole table and constant; and startup wiring for the new table.

Changes

Cohort / File(s) Summary
API proto & routes
api/org-unit-role.proto, api/org-unit-role.pb.route.go
Added RPCs CreateCustomRole/UpdateCustomRole/GetCustomRole/DeleteCustomRole with HTTP mappings and resource metadata; added messages ResourceMatch, RolePermission, Create/Update/Get/Delete request & response types; extended OrgUnitRolesListEntry with type, displayName, created, createdBy; added routes under /api/auth/v1/ou/{ou}/role.
Swagger / OpenAPI
api/swagger/apidocs.swagger.json
Added paths and schemas for custom-role CRUD bodies and responses; introduced apiResourceMatch and apiRolePermission; extended role list entry/response metadata and descriptions.
Gateway routing & filtering
pkg/gateway/routes.go, pkg/gateway/server.go
Extended per-route data with resource and verb; added RouteInfo extraction; introduced filteringResponseWriter buffering, smart pagination, and two-pass deny-before-allow PBAC evaluation; wired gateway to consult ouCustomRole table for resource-matching and per-route filtering.
Server handlers
pkg/server/org-unit-role.go, pkg/server/org-unit-user.go
Implemented Create/Update/Get/Delete custom-role RPCs, validation helpers, permission conversions, and List augmentation (up to 1000 custom roles); wired custom-role table into servers; org-unit-user now validates role names and permanently deletes orphaned soft-deleted custom roles when appropriate.
Table & model
pkg/table/org-unit-custom-role.go, pkg/table/const.go
Added OrgUnitCustomRoleCollectionName constant and a Mongo-backed model/table: OrgUnitCustomRoleKey, ResourceMatch, RolePermission, OrgUnitCustomRole, OrgUnitCustomRoleTable; CRUD/query methods, soft-delete/permanent-delete semantics, binding checks (HasBindings), cleanup of orphaned soft-deleted roles, and locator/init helpers.
App init
main.go
Startup now locates the OrgUnitCustomRole table (LocateOrgUnitCustomRoleTable) and wires it into initialization; panics on locate error.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Gateway
  participant OrgUnitRoleService
  participant OrgUnitCustomRoleTable
  participant Mongo
  rect rgba(230,240,255,0.6)
    note right of Client: Custom-role CRUD request
    Client->>Gateway: HTTP /api/auth/v1/ou/{ou}/role[/{name}]
    Gateway->>OrgUnitRoleService: gRPC Create/Update/Get/Delete
    alt Create
      OrgUnitRoleService->>OrgUnitCustomRoleTable: Insert OrgUnitCustomRole
      OrgUnitCustomRoleTable->>Mongo: write
      Mongo-->>OrgUnitCustomRoleTable: ack
      OrgUnitCustomRoleTable-->>OrgUnitRoleService: created
      OrgUnitRoleService-->>Gateway: CreateCustomRoleResp
    else Read/Update/Delete
      OrgUnitRoleService->>OrgUnitCustomRoleTable: Find/Update/Delete (binding checks)
      OrgUnitCustomRoleTable->>Mongo: query/write
      Mongo-->>OrgUnitCustomRoleTable: result
      OrgUnitCustomRoleTable-->>OrgUnitRoleService: outcome
      OrgUnitRoleService-->>Gateway: Resp
    end
    Gateway-->>Client: HTTP response
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Gateway
  participant OrgUnitCustomRoleTable
  rect rgba(240,255,240,0.6)
    note right of User: Gateway enforces resource-level PBAC for list/detail endpoints
    User->>Gateway: HTTP request to OU-protected endpoint
    Gateway->>Gateway: extract RouteInfo {resource, verb, resourceName}
    alt resource references a custom-role name
      Gateway->>OrgUnitCustomRoleTable: FindByNameAndOrgUnit(tenant, ou, name)
      OrgUnitCustomRoleTable-->>Gateway: customRole (permissions)
      Gateway->>Gateway: evaluate deny-before-allow rules
      alt allowed
        Gateway->>User: forward request / 200
      else denied
        Gateway-->>User: 403 Forbidden
      end
    else listing endpoint
      Gateway->>Gateway: wrap response in filteringResponseWriter (buffer)
      Gateway->>OrgUnitCustomRoleTable: fetch rules as needed for filtering pages
      Gateway-->>User: filtered list response
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

I hop through routes and plant a tiny role,
Match carrots to resources, permissions make me whole.
Gateway guards the garden, servers tend the soil,
Tables hold the harvest safe from toil.
A nibble, a patch — a rabbit's joyful role. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Feature: Add org‑unit scoped custom role management with full CRUD and permission checks" directly and accurately reflects the main changes in the changeset. The PR introduces complete CRUD functionality for custom roles scoped to organization units, along with permission-based filtering and access controls. The title is specific and descriptive, clearly communicating the feature's scope and capabilities without being vague or overly broad. The terminology used aligns with the actual implementation across API definitions, routing, server logic, and database layers.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
main.go (1)

401-410: Variable shadowing ‘client’ breaks types; rename Keycloak client.

client is used for the DB earlier; here it’s re-declared for Keycloak, likely causing type confusion for subsequent calls.

Apply:

-	// create a new keycloak client
-	client, err := keycloak.New(conf.GetKeycloakEndpoint())
+	// create a new keycloak client
+	kcClient, err := keycloak.New(conf.GetKeycloakEndpoint())
 	if err != nil {
 		// failed to create keycloak client, nothing more can be done
 		log.Panicf("failed to create keycloak client: %s", err)
 	}
 	defer func() {
-		_ = client.Logout(context.Background())
+		_ = kcClient.Logout(context.Background())
 	}()
-	_, err = tenant.NewSetupController(client)
+	_, err = tenant.NewSetupController(kcClient)
 	...
-	_, err = tenant.NewRoleController(client)
+	_, err = tenant.NewRoleController(kcClient)
 	...
-	_, err = tenant.NewAdminController(client)
+	_, err = tenant.NewAdminController(kcClient)

Optionally consider starting an event logger for the Org Unit Custom Role table (if supported) to match OU/OU-User.

Also applies to: 419-435

pkg/server/org-unit-user.go (1)

67-71: Inconsistent role validation and no custom roles on Add; include ‘default’ and reuse validateRole.

  • Add path only allows built-in roles; should accept valid custom roles too.
  • validateRole treats default as invalid; Update rejects default.

Apply:

 func (s *OrgUnitUserServer) AddOrgUnitUser(ctx context.Context, req *api.OrgUnitUserAddReq) (*api.OrgUnitUserAddResp, error) {
   authInfo, _ := auth.GetAuthInfoFromContext(ctx)
   if authInfo == nil {
     return nil, status.Errorf(codes.Unauthenticated, "User not authenticated")
   }

-  // validate role, currently only admin, default and auditor roles are allowed
-  if req.Role != "admin" && req.Role != "default" && req.Role != "auditor" {
-    return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
-  }
+  // validate role (built-in or custom)
+  if err := s.validateRole(ctx, req.Role, req.Ou, authInfo.Realm); err != nil {
+    return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
+  }
 func (s *OrgUnitUserServer) validateRole(ctx context.Context, roleName, orgUnitId, tenant string) error {
   // Check if it's a built-in system role
-  if roleName == "admin" || roleName == "auditor" {
+  if roleName == "admin" || roleName == "auditor" || roleName == "default" {
     return nil // Built-in roles are always valid
   }

Also applies to: 104-107, 245-261

🧹 Nitpick comments (18)
api/org-unit-role.proto (5)

92-102: Use enums for role type and Timestamps for dates; align field naming.

  • type should be an enum (e.g., ROLE_TYPE_BUILTIN, ROLE_TYPE_CUSTOM), not a free-form string.
  • created should use google.protobuf.Timestamp, not int64.
  • desc is inconsistent with description elsewhere; prefer description for consistency.

Apply these diffs:

-  // role type: "built-in" for system roles (admin, auditor) or "custom" for user-defined roles
-  string type = 3;
+  // role type: built-in or custom
+  enum RoleType {
+    ROLE_TYPE_UNSPECIFIED = 0;
+    ROLE_TYPE_BUILTIN = 1;
+    ROLE_TYPE_CUSTOM = 2;
+  }
+  RoleType type = 3;

-  // role description, provding details about the role
-  string desc = 2;
+  // role description, providing details about the role
+  string description = 2;

-  // creation timestamp for custom roles (only applicable for custom roles)
-  int64 created = 5;
+  // creation timestamp for custom roles (only applicable for custom roles)
+  google.protobuf.Timestamp created = 5;

Also add the import:

 import "google/api/annotations.proto";
 import "coreapis/api/role.proto";
+import "google/protobuf/timestamp.proto";

156-164: Support partial updates with a FieldMask (or mark fields optional).

Without presence (proto3) you can’t distinguish “unset” vs “set empty,” making PUT updates risky. Prefer PATCH + FieldMask or keep PUT but require full object; if partial updates are intended, add FieldMask.

Example:

+import "google/protobuf/field_mask.proto";
 message UpdateCustomRoleReq {
   // Organization unit ID
   string ou = 1;

   // Name of the custom role to update
   string name = 2;

   // Updated display name for the custom role
-  string displayName = 3;
+  string displayName = 3;

   // Updated description explaining the purpose of this custom role
-  string description = 4;
+  string description = 4;

   // Updated list of permissions granted by this custom role
   repeated RolePermission permissions = 5;
+
+  // Fields to update (if supporting partial updates)
+  google.protobuf.FieldMask update_mask = 6;
 }

If adopting PATCH, change the HTTP annotation accordingly.


195-206: Use google.protobuf.Timestamp for created/updated in GetCustomRoleResp.

Timestamps as int64 are ambiguous (seconds vs millis) and less interoperable.

-  // Timestamp when the role was created
-  int64 created = 5;
+  // Timestamp when the role was created
+  google.protobuf.Timestamp created = 5;

-  // Timestamp when the role was last updated
-  int64 updated = 7;
+  // Timestamp when the role was last updated
+  google.protobuf.Timestamp updated = 7;

Ensure you have:

import "google/protobuf/timestamp.proto";

65-67: Remove duplicated comment.

The Delete comment is repeated twice.

-  // Delete a custom role from the organization unit
-  // Delete (soft delete) a custom role from the organization unit
+  // Delete (soft delete) a custom role from the organization unit

115-122: Consider constraining verbs to a known set.

repeated string verbs is flexible but error-prone. If you have a canonical verb set, use an enum or validate strictly server-side.

pkg/server/org-unit-user.go (1)

141-195: Streamline pre-delete fetch and guard nils.

  • Prefer a typed Get/Find by key over a raw bson filter + FindMany(…, 1) if available, to reduce coupling.
  • When handling existingRole from FindAnyByNameAndOrgUnit, guard against unexpected nils before dereferencing Active.

Example:

// Prefer: user, err := s.tbl.Find(ctx, key)
// and check err == errors.NotFound, else use user.Role.
pkg/gateway/server.go (1)

263-275: Support wildcards and case-insensitive verbs in custom-role checks.

Small enhancement to make permissions more ergonomic and less brittle to verb casing.

Apply this diff:

 func (s *gateway) validatePermissions(customRole *table.OrgUnitCustomRole, routeInfo *RouteInfo) bool {
 	// Check each permission in the custom role
 	for _, permission := range customRole.Permissions {
-		if permission.Resource == routeInfo.Resource {
+		if permission.Resource == routeInfo.Resource || permission.Resource == "*" {
 			// Check if the required verb is allowed
 			for _, allowedVerb := range permission.Verbs {
-				if allowedVerb == routeInfo.Verb {
+				if strings.EqualFold(allowedVerb, routeInfo.Verb) || allowedVerb == "*" {
 					return true // Permission granted
 				}
 			}
 		}
 	}
 
 	// If no matching permission found, deny access
 	return false
 }
api/swagger/apidocs.swagger.json (5)

302-317: Tighten DELETE summary and move behavior detail to description.

Duplicate summary text; move soft-delete note to description.

-        "summary": "Delete a custom role from the organization unit\nDelete (soft delete) a custom role from the organization unit",
+        "summary": "Delete a custom role from the organization unit",
+        "description": "Soft deletes when bindings exist; permanently deletes otherwise.",

3538-3553: Constrain verbs array and ensure uniqueness.

Add minItems and uniqueItems to reduce invalid inputs; mirrors server’s “at least one verb” requirement.

     "apiRolePermission": {
       "type": "object",
       "properties": {
         "resource": {
           "type": "string",
           "title": "Resource name this permission applies to"
         },
         "verbs": {
           "type": "array",
+          "minItems": 1,
+          "uniqueItems": true,
           "items": {
             "type": "string"
           },
           "title": "List of allowed verbs/actions for this resource"
         }
       },
       "title": "Permission definition for custom roles"
     },

3347-3367: Fix typo in role description.

“provding” → “providing”.

-        "desc": {
-          "type": "string",
-          "title": "role description, provding details about the role"
-        },
+        "desc": {
+          "type": "string",
+          "title": "role description, providing details about the role"
+        },

223-262: Consider 201 for create endpoint.

POST create responses typically use 201 Created. Optional, but improves API semantics.


2433-2456: OpenAPI refs: avoid setting both $ref and type in items.

When using $ref, omit the sibling "type": "object" under items. Several new arrays include both; many validators will ignore type, but cleaning this up reduces lint noise.

Also applies to: 2460-2477, 2786-2807

pkg/server/org-unit-role.go (4)

37-55: List roles: OK, but consider pagination for custom roles.

Fetching up to 1000 items is a guard, but adding offset/limit to the request and returning count would scale better.

Also applies to: 56-74


139-152: Use a named bool for the Active pointer (readability).

Minor readability improvement.

-	customRole := &table.OrgUnitCustomRole{
+	active := true
+	customRole := &table.OrgUnitCustomRole{
 		Key: &table.OrgUnitCustomRoleKey{
 			Tenant:    authInfo.Realm,
 			OrgUnitId: req.Ou,
 			Name:      req.Name,
 		},
 		DisplayName: req.DisplayName,
 		Description: req.Description,
 		Permissions: permissions,
 		Created:     time.Now().Unix(),
 		CreatedBy:   authInfo.UserName,
-		Active:      &[]bool{true}[0], // Mark role as active
+		Active:      &active, // Mark role as active
 	}

336-390: Create request validation matches API; consider character constraints.

Add a simple pattern (e.g., ^[a-zA-Z][a-zA-Z0-9_-]{1,49}$) for Name to avoid spaces and odd chars; this can be mirrored in Swagger.


410-434: Update validation mirrors Create; consider deduping and canonicalization.

  • Trim spaces and dedupe verbs per permission.
  • Optionally validate resource + verb against route definitions to catch typos early.
pkg/table/org-unit-custom-role.go (2)

139-160: Bindings check relies on OrgUnitUser indexes; ensure supporting index.

Counting by tenant+orgUnitId+role will need an index for scale.

Would you confirm there’s an index on {"key.tenant", "key.orgUnitId", "role"} in OrgUnitUser collection?


16-16: Optional: guard singleton init with sync.Once.

Minor race window if Locate is called concurrently.

Also applies to: 250-269

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 064ef50 and 4065bbc7478ad4a50ce5c27980871a5b493c2c72.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (5 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🧰 Additional context used
🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2448-2455: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (15)
api/org-unit-role.pb.route.go (1)

20-46: Routes for custom roles look consistent with RPCs and scopes.

Resource and verbs align with proto annotations; paths are coherent.

Please re-run route generation to ensure this file remains in sync with proto changes and swagger (if not already automated).

pkg/table/const.go (1)

30-32: LGTM: added collection name.

Constant name and value align with naming convention.

pkg/gateway/routes.go (1)

26-28: Good addition: carry resource and verb per route.

This enables gateway-level permission checks without extra lookups.

Confirm downstream permission checks consume these fields (e.g., in gateway/server) and handle empty values defensively for legacy routes.

Also applies to: 54-56, 67-69

pkg/server/org-unit-user.go (1)

206-215: LGTM: wiring custom role table.

Table retrieval and panic-on-error are consistent with existing patterns.

pkg/gateway/server.go (4)

47-56: LGTM: wiring of custom-role table into gateway.

Initialization and field addition look correct.


224-234: LGTM: built-in OU roles mapped to read/write semantics.

Admin wildcard and auditor read-only checks are straightforward and correct.


300-310: LGTM: defer-based access logging hook.

This ensures access logs even on early returns with proper context captured.


535-538: LGTM: custom-role table retrieval and gateway construction.

Table retrieval with panic-on-error and dependency injection into gateway is appropriate.

Also applies to: 555-562

api/swagger/apidocs.swagger.json (1)

2448-2455: Address Checkov CKV_OPENAPI_21: add a maximum for permissions array.

Handled by adding maxItems: 20 above, which matches server constraints.

Please re-run your OpenAPI linter/Checkov to confirm the warning is resolved.

pkg/server/org-unit-role.go (4)

92-96: Reserved names check: good coverage.

Blocking admin/default/auditor prevents clashes with built-ins.


98-134: Name reuse with soft-deletes: solid, binding-aware flow.

The “find any” + bindings check + permanent delete unblocks ghost entries safely.


175-191: Confirm Update semantics (partial vs. replace).

updateRole omits Key and Active; ensure table.Update performs a partial $set and doesn’t overwrite the entire doc. If it’s a full replace, this will drop Key and Active.

Can you confirm the behavior of table.Update in github.com/go-core-stack/core/table? If it’s a partial update, we’re good; if not, we should switch to a patch/merge update call.

Also applies to: 199-207


299-309: LGTM: table acquisition at server init.

Panicking on missing table ensures startup fails fast.

pkg/table/org-unit-custom-role.go (2)

64-79: Queries correctly filter active roles (nil or true).

Consistent use of active != false across getters is good.

Also applies to: 91-105, 106-127


167-182: Deletion strategy is sound.

Soft-delete on bindings and hard-delete otherwise is the right trade-off.

Comment on lines +2433 to +2458
"OrgUnitRoleCreateCustomRoleBody": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Name of the custom role (must be unique within the org unit)"
},
"displayName": {
"type": "string",
"title": "Display name for the custom role"
},
"description": {
"type": "string",
"title": "Description explaining the purpose of this custom role"
},
"permissions": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/apiRolePermission"
},
"title": "List of permissions granted by this custom role"
}
},
"title": "Create custom role request"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align schema with server validation: require fields and cap permissions array.

Server enforces non-empty name/displayName/description/permissions and max 20 permissions. Reflect this in OpenAPI to prevent client/server divergence.

     "OrgUnitRoleCreateCustomRoleBody": {
       "type": "object",
       "properties": {
         "name": {
           "type": "string",
           "title": "Name of the custom role (must be unique within the org unit)"
         },
         "displayName": {
           "type": "string",
           "title": "Display name for the custom role"
         },
         "description": {
           "type": "string",
           "title": "Description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "List of permissions granted by this custom role"
         }
       },
+      "required": ["name","displayName","description","permissions"],
       "title": "Create custom role request"
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"OrgUnitRoleCreateCustomRoleBody": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Name of the custom role (must be unique within the org unit)"
},
"displayName": {
"type": "string",
"title": "Display name for the custom role"
},
"description": {
"type": "string",
"title": "Description explaining the purpose of this custom role"
},
"permissions": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/apiRolePermission"
},
"title": "List of permissions granted by this custom role"
}
},
"title": "Create custom role request"
},
"OrgUnitRoleCreateCustomRoleBody": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Name of the custom role (must be unique within the org unit)"
},
"displayName": {
"type": "string",
"title": "Display name for the custom role"
},
"description": {
"type": "string",
"title": "Description explaining the purpose of this custom role"
},
"permissions": {
"type": "array",
"minItems": 1,
"maxItems": 20,
"items": {
"$ref": "#/definitions/apiRolePermission"
},
"title": "List of permissions granted by this custom role"
}
},
"required": ["name", "displayName", "description", "permissions"],
"title": "Create custom role request"
},
🧰 Tools
🪛 Checkov (3.2.334)

[medium] 2448-2455: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🤖 Prompt for AI Agents
In api/swagger/apidocs.swagger.json around lines 2433 to 2458, the
OrgUnitRoleCreateCustomRoleBody schema lacks required constraints and a cap on
the permissions array; update the schema to mark
"name","displayName","description","permissions" as required, add minLength: 1
(or equivalent non-empty constraint) to the three string properties, and set
permissions to have minItems: 1 and maxItems: 20 (with items unchanged) so the
OpenAPI contract matches server validation.

Comment on lines +2460 to +2480
"type": "object",
"properties": {
"displayName": {
"type": "string",
"title": "Updated display name for the custom role"
},
"description": {
"type": "string",
"title": "Updated description explaining the purpose of this custom role"
},
"permissions": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/apiRolePermission"
},
"title": "Updated list of permissions granted by this custom role"
}
},
"title": "Update custom role request"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make update fields required and cap permissions array.

Server requires displayName/description/permissions and limits permissions to 20.

     "OrgUnitRoleUpdateCustomRoleBody": {
       "type": "object",
       "properties": {
         "displayName": {
           "type": "string",
           "title": "Updated display name for the custom role"
         },
         "description": {
           "type": "string",
           "title": "Updated description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "Updated list of permissions granted by this custom role"
         }
       },
+      "required": ["displayName","description","permissions"],
       "title": "Update custom role request"
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"type": "object",
"properties": {
"displayName": {
"type": "string",
"title": "Updated display name for the custom role"
},
"description": {
"type": "string",
"title": "Updated description explaining the purpose of this custom role"
},
"permissions": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/apiRolePermission"
},
"title": "Updated list of permissions granted by this custom role"
}
},
"title": "Update custom role request"
},
"type": "object",
"properties": {
"displayName": {
"type": "string",
"title": "Updated display name for the custom role"
},
"description": {
"type": "string",
"title": "Updated description explaining the purpose of this custom role"
},
"permissions": {
"type": "array",
"minItems": 1,
"maxItems": 20,
"items": {
"$ref": "#/definitions/apiRolePermission"
},
"title": "Updated list of permissions granted by this custom role"
}
},
"required": ["displayName","description","permissions"],
"title": "Update custom role request"
},
🤖 Prompt for AI Agents
In api/swagger/apidocs.swagger.json around lines 2460 to 2480, the Update custom
role request schema must mark displayName, description, and permissions as
required and enforce the server-side limit of 20 permissions; add a "required":
["displayName","description","permissions"] entry to the object and add
"maxItems": 20 to the permissions array definition (optionally include
"minItems": 1 if the API requires at least one permission).

Comment on lines +241 to +249
// GetOrgUnitCustomRoleTable returns the global custom role table instance
func GetOrgUnitCustomRoleTable() (*OrgUnitCustomRoleTable, error) {
if orgUnitCustomRoleTable != nil {
return orgUnitCustomRoleTable, nil
}

return nil, errors.Wrapf(errors.NotFound, "org unit custom role table not found")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Ensure unique index on (tenant, orgUnitId, name).

To prevent duplicates under concurrent create, enforce a unique index at init if not already present in table.Initialize.

Please confirm Initialize sets a unique compound index on key.tenant+key.orgUnitId+key.name; if not, add it in LocateOrgUnitCustomRoleTable.

Also applies to: 250-269


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "--- find file ---"
rg -n --hidden "org-unit-custom-role.go" || true

echo "--- show file (pkg/table/org-unit-custom-role.go) if present ---"
if [ -f "pkg/table/org-unit-custom-role.go" ]; then
  sed -n '1,220p' pkg/table/org-unit-custom-role.go
  echo "----- lines 221-440 -----"
  sed -n '221,440p' pkg/table/org-unit-custom-role.go
else
  echo "pkg/table/org-unit-custom-role.go not found"
fi

echo "--- search for related symbols ---"
rg -n --hidden "OrgUnitCustomRoleTable" || true
rg -n --hidden "LocateOrgUnitCustomRoleTable" || true
rg -n --hidden "GetOrgUnitCustomRoleTable" || true

echo "--- search for Initialize implementations in repo ---"
rg -n --hidden "func .*Initialize" --type-add 'go:*.go' -g '!**/vendor/**' || true

echo "--- search for index/index creation patterns mentioning tenant/orgUnit/name ---"
rg -n --hidden -S "CreateIndex|EnsureIndex|CreateIndexes|EnsureIndexes|Index\(" || true
rg -n --hidden -S "tenant.*orgUnitId.*name|tenant.*name.*orgUnitId|orgUnitId.*tenant.*name|orgUnitId.*name.*tenant|name.*tenant.*orgUnitId" || true

echo "--- search for 'tenant' usage in pkg/table ---"
rg -n --hidden "tenant" pkg/table || true

Length of output: 18095


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "----- show tenant.go -----"
sed -n '1,260p' pkg/table/tenant.go || true
echo "----- show org-unit.go -----"
sed -n '1,260p' pkg/table/org-unit.go || true
echo "----- show org-unit-user.go -----"
sed -n '1,320p' pkg/table/org-unit-user.go || true
echo "----- show user.go -----"
sed -n '1,320p' pkg/table/user.go || true
echo "----- show api-keys.go -----"
sed -n '1,320p' pkg/table/api-keys.go || true

echo "----- search for .Initialize( across repo -----"
rg -n "\.Initialize\(" || true

echo "----- search for EnsureIndex|CreateIndex|CreateIndexes|Index across repo -----"
rg -n -S "EnsureIndex|CreateIndex|CreateIndexes|CreateIndexes|Index\(" || true

echo "----- list Locate*Table functions -----"
rg -n "Locate.*Table" || true

Length of output: 19705


🌐 Web query:

github.com/go-core-stack/core table Initialize Table Initialize create index

💡 Result:

Short answer:

  • To initialize a Table from go-core-stack/core you must call its Initialize(col db.StoreCollection) method with a db.StoreCollection implementation before using the table. Initialize performs sanity checks and registers the key type with the collection. [1]

  • The table package does not create database indexes for you; index creation belongs to your backing store (migrations or the db.StoreCollection implementation). Create indexes in your DB (SQL CREATE INDEX / Mongo createIndex, or via your store's migration code) or implement index-creation in the StoreCollection you pass to Initialize. [1][2]

References:

  1. table.Initialize documentation — github.com/go-core-stack/core/table. [pkg.go.dev entry]. [1]
  2. Example guidance for creating DB tables/indexes (do it in DB/migrations or connection setup) — MySQL DB ops docs (example of creating tables / initialize DB connections). [2]

Add unique compound index on (key.tenant, key.orgUnitId, key.name)

go-core-stack's Table.Initialize doesn't create DB indexes and this repo contains no index-creation for this collection — add a unique compound index (tenant + orgUnitId + name) to prevent duplicate rows under concurrent creates. Add the index in LocateOrgUnitCustomRoleTable (pkg/table/org-unit-custom-role.go) during initialization or manage it via DB migrations/connection setup.

🤖 Prompt for AI Agents
In pkg/table/org-unit-custom-role.go around lines 241 to 249, the repo lacks a
unique compound index to prevent concurrent duplicate creates; modify
LocateOrgUnitCustomRoleTable (the table initialization path) to ensure a unique
compound index on the underlying collection for fields key.tenant,
key.orgUnitId, and key.name (or create it as part of DB migrations/connection
setup if preferred). Specifically, when creating/assigning
orgUnitCustomRoleTable, call the DB driver's index creation API to create a
unique compound index on (tenant, orgUnitId, name) with appropriate options
(unique=true, background if supported) and handle/index-exists errors gracefully
so initialization is idempotent. Ensure the index creation is executed before
returning the table instance.

@Suryanshu185 Suryanshu185 force-pushed the main branch 2 times, most recently from 740efaa to 37ac535 Compare October 14, 2025 06:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/server/org-unit-user.go (1)

61-96: Update AddOrgUnitUser to use the validateRole helper.

The AddOrgUnitUser method still uses hardcoded role validation (lines 68-70), while UpdateOrgUnitUser uses the new validateRole helper. This creates inconsistency in role validation logic across the two methods.

Apply this diff to use the consistent validation approach:

 func (s *OrgUnitUserServer) AddOrgUnitUser(ctx context.Context, req *api.OrgUnitUserAddReq) (*api.OrgUnitUserAddResp, error) {
 	authInfo, _ := auth.GetAuthInfoFromContext(ctx)
 	if authInfo == nil {
 		return nil, status.Errorf(codes.Unauthenticated, "User not authenticated")
 	}
 
-	// validate role, currently only admin, default and auditor roles are allowed
-	if req.Role != "admin" && req.Role != "default" && req.Role != "auditor" {
-		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
+	// validate role - check if it's a built-in role or custom role
+	if err := s.validateRole(ctx, req.Role, req.Ou, authInfo.Realm); err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s - %v", req.Role, err)
 	}
♻️ Duplicate comments (1)
api/swagger/apidocs.swagger.json (1)

2481-2528: Address the previously flagged schema validation issues.

The OpenAPI schema for custom role creation and update bodies still lacks the constraints enforced by server-side validation. This was previously flagged in past review comments but hasn't been addressed yet.

Server validation (in pkg/server/org-unit-role.go lines 337-434) enforces:

  • Required fields: name, displayName, description, permissions (for create); displayName, description, permissions (for update)
  • Non-empty string constraints for name, displayName, description
  • Permissions array: minItems: 1, maxItems: 20

Please apply the fixes suggested in the past review comments to align the OpenAPI schema with server validation.

🧹 Nitpick comments (2)
pkg/server/org-unit-role.go (2)

29-79: Consider adding pagination support for custom roles.

The hardcoded limit of 1000 custom roles (line 57) could be problematic for organizations with many custom roles. The method doesn't support pagination parameters from the request.

Consider either:

  1. Adding offset/limit parameters to the OrgUnitRolesListReq message and using them here
  2. Documenting the 1000-role limit in the proto file comments
  3. Deferring pagination to a future enhancement if 1000 roles is sufficient for the initial release

For now, the graceful degradation (returning built-in roles even if custom role fetch fails) is good defensive programming.


81-166: Use a more idiomatic approach for the boolean pointer.

Line 150 uses Active: &[]bool{true}[0] to create a pointer to true, which is clever but non-idiomatic and less readable.

Consider using a helper variable instead:

+	active := true
 	customRole := &table.OrgUnitCustomRole{
 		Key: &table.OrgUnitCustomRoleKey{
 			Tenant:    authInfo.Realm,
 			OrgUnitId: req.Ou,
 			Name:      req.Name,
 		},
 		DisplayName: req.DisplayName,
 		Description: req.Description,
 		Permissions: permissions,
 		Created:     time.Now().Unix(),
 		CreatedBy:   authInfo.UserName,
-		Active:      &[]bool{true}[0], // Mark role as active
+		Active:      &active,
 	}

The rest of the method correctly handles:

  • Reserved name validation
  • Existing role checks (including soft-deleted roles)
  • Cleanup of orphaned soft-deleted roles before recreation
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 4065bbc7478ad4a50ce5c27980871a5b493c2c72 and 37ac535b022ac7c1b520987bf2b15ba37bc149d4.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (5 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/gateway/routes.go
  • pkg/table/const.go
  • api/org-unit-role.pb.route.go
  • pkg/gateway/server.go
  • pkg/table/org-unit-custom-role.go
🧰 Additional context used
🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2496-2503: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (10)
main.go (1)

392-396: LGTM!

The initialization of the Org Unit Custom Role table follows the established pattern used for other tables in this file, with appropriate error handling.

pkg/server/org-unit-user.go (2)

129-198: LGTM!

The enhanced deletion flow correctly:

  • Pre-fetches user data to preserve the role for potential cleanup
  • Deletes the user first
  • Then performs cleanup of orphaned soft-deleted custom roles with proper error handling

The non-fatal error handling for cleanup is appropriate since the primary operation (user deletion) has already succeeded.


200-242: LGTM!

The server initialization correctly wires in the custom role table with proper error handling.

api/org-unit-role.proto (1)

1-221: LGTM!

The protobuf definitions for custom role management are well-structured and comprehensive:

  • Complete CRUD surface with clear documentation
  • Consistent naming conventions
  • RolePermission message properly models per-role permissions
  • Extended OrgUnitRolesListEntry includes necessary metadata for both built-in and custom roles
pkg/server/org-unit-role.go (6)

168-212: LGTM!

The update operation is straightforward and correctly handles authentication, validation, permission conversion, and error cases.


214-244: LGTM!

The retrieval operation correctly fetches the role, converts permissions back to the protobuf format, and handles error cases appropriately.


246-273: LGTM!

The delete operation correctly delegates the binding-aware deletion logic to the table layer, which handles the complexity of deciding between soft delete and permanent delete based on whether bindings exist.


275-297: LGTM!

The permission conversion helpers are straightforward and correctly translate between protobuf and table representations.


299-335: LGTM!

The server initialization correctly:

  • Retrieves the custom role table with panic on failure (appropriate for initialization)
  • Initializes the server struct with the table reference
  • Registers gRPC services and HTTP routes

337-434: LGTM!

The validation helpers are comprehensive and correctly enforce:

  • Name, displayName, and description length constraints
  • Permissions array size (1-20)
  • Non-empty resource and verb values within permissions

These constraints match the requirements that should be reflected in the OpenAPI schema (as noted in the swagger file review).

Comment thread pkg/server/org-unit-user.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
api/swagger/apidocs.swagger.json (2)

2481-2506: Schema constraints should match server validation.

The schema lacks required field markers and array size constraints that the server enforces. The server requires all fields to be non-empty and limits permissions to 20 items (see pkg/server/org-unit-role.go lines 337-390).

Apply this diff to align the schema with server validation:

     "OrgUnitRoleCreateCustomRoleBody": {
       "type": "object",
       "properties": {
         "name": {
           "type": "string",
+          "minLength": 2,
+          "maxLength": 50,
           "title": "Name of the custom role (must be unique within the org unit)"
         },
         "displayName": {
           "type": "string",
+          "minLength": 1,
+          "maxLength": 100,
           "title": "Display name for the custom role"
         },
         "description": {
           "type": "string",
+          "minLength": 1,
+          "maxLength": 500,
           "title": "Description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "List of permissions granted by this custom role"
         }
       },
+      "required": ["name", "displayName", "description", "permissions"],
       "title": "Create custom role request"
     },

2507-2528: Update schema constraints should match server validation.

Similar to the create schema, the update schema lacks required field markers and array size constraints that the server enforces (see pkg/server/org-unit-role.go lines 392-434).

Apply this diff:

     "OrgUnitRoleUpdateCustomRoleBody": {
       "type": "object",
       "properties": {
         "displayName": {
           "type": "string",
+          "minLength": 1,
+          "maxLength": 100,
           "title": "Updated display name for the custom role"
         },
         "description": {
           "type": "string",
+          "minLength": 1,
+          "maxLength": 500,
           "title": "Updated description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "Updated list of permissions granted by this custom role"
         }
       },
+      "required": ["displayName", "description", "permissions"],
       "title": "Update custom role request"
     },
🧹 Nitpick comments (3)
pkg/gateway/server.go (1)

262-278: Consider short-circuiting on resource match.

The nested loop iterates all permissions and verbs. While correct, you could slightly optimize by breaking the inner loop once the resource matches but before checking all verbs:

for _, permission := range customRole.Permissions {
    if permission.Resource == routeInfo.Resource {
        for _, allowedVerb := range permission.Verbs {
            if allowedVerb == routeInfo.Verb {
                return true
            }
        }
        // Resource matched but verb didn't - no need to check other permissions
        break
    }
}

This optimization is minor and can be deferred.

pkg/server/org-unit-user.go (1)

141-161: Consider using Find instead of FindMany.

The code uses FindMany with limit=1 to fetch a single user. Since you're looking up by a unique key, consider using a direct Find method if available, which would be more semantically clear:

// Get the user data before deletion to check their role
userData, err := s.tbl.Find(ctx, key)
if err != nil {
    if errors.IsNotFound(err) {
        return nil, status.Errorf(codes.NotFound, "Org Unit User %s, not found", req.User)
    }
    log.Printf("failed to get org unit user before deletion, got error: %s", err)
    return nil, status.Errorf(codes.Internal, "Something went wrong, please try again later")
}

If Find is not available or doesn't fit the table API, the current approach is acceptable.

pkg/server/org-unit-role.go (1)

56-73: Hardcoded limit of 1000 custom roles is reasonable for now.

The query fetches up to 1000 custom roles. While this is a reasonable limit for most use cases, consider making it configurable or documenting this limitation if you expect organizations with more custom roles in the future.

Do you want me to help document this limitation or make the limit configurable?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 37ac535b022ac7c1b520987bf2b15ba37bc149d4 and 2180a27b45c00cfed51206017269178273707919.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (5 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • api/org-unit-role.pb.route.go
  • main.go
  • pkg/table/const.go
  • pkg/table/org-unit-custom-role.go
  • pkg/gateway/routes.go
🧰 Additional context used
🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2496-2503: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (4)
pkg/gateway/server.go (2)

236-248: LGTM: Custom role integration is correctly implemented.

The custom role lookup is properly positioned after built-in role checks, with appropriate error handling and denial on not found. The integration with checkCustomRolePermissions ensures permission validation.


286-307: LGTM: Path selection logic correctly mirrors ServeHTTP.

The implementation now properly handles encoded paths by selecting RawPath when available, falling back to Path otherwise. This ensures consistent route matching with ServeHTTP and addresses the previous review feedback.

pkg/server/org-unit-user.go (1)

244-261: LGTM: "default" role correctly included in built-in check.

The validation now properly recognizes "admin", "default", and "auditor" as built-in roles, addressing the previous review feedback. The custom role validation path is correctly implemented with appropriate error handling.

pkg/server/org-unit-role.go (1)

81-166: LGTM: Create logic correctly handles existing and soft-deleted roles.

The implementation properly:

  • Validates input and rejects reserved names
  • Checks for existing active roles
  • Handles soft-deleted roles: permits reuse only if no bindings exist
  • Permanently deletes orphaned soft-deleted roles before recreation

The active pointer initialization at line 150 (&[]bool{true}[0]) is idiomatic for creating a pointer to a boolean literal.

Comment thread api/swagger/apidocs.swagger.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
api/org-unit-role.proto (1)

339-371: Validate role name charset to be path-safe.

Name appears in a path segment; disallow slashes/unsafe chars to prevent routing issues.

 import (
   "context"
   "fmt"
   "log"
   "time"
+  "regexp"
   ...
 )

 func (s *OrgUnitRoleServer) validateCreateCustomRoleRequest(req *api.CreateCustomRoleReq) error {
   // Validate role name
   if req.Name == "" {
     return errors.New("role name cannot be empty")
   }
   if len(req.Name) < 2 {
     return errors.New("role name must be at least 2 characters long")
   }
   if len(req.Name) > 50 {
     return errors.New("role name cannot exceed 50 characters")
   }
+  // allow letters, digits, hyphen, underscore, dot only
+  if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+    return errors.New("role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+  }
   ...
 }

Also applies to: 374-390

pkg/server/org-unit-user.go (1)

67-70: AddOrgUnitUser blocks custom roles; use validateRole like Update.

This prevents assigning newly created custom roles. Replace hard-coded check.

-  // validate role, currently only admin, default and auditor roles are allowed
-  if req.Role != "admin" && req.Role != "default" && req.Role != "auditor" {
-    return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
-  }
+  // validate role (built-in or custom)
+  if err := s.validateRole(ctx, req.Role, req.Ou, authInfo.Realm); err != nil {
+    return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s - %v", req.Role, err)
+  }
♻️ Duplicate comments (3)
api/swagger/apidocs.swagger.json (3)

2481-2506: Align Create body schema with server validation (required + array caps).

Mark fields required and bound permissions length. Also, drop redundant "type": "object" inside items.

     "OrgUnitRoleCreateCustomRoleBody": {
       "type": "object",
       "properties": {
         "name": { "type": "string", "title": "Name of the custom role (must be unique within the org unit)" },
         "displayName": { "type": "string", "title": "Display name for the custom role" },
         "description": { "type": "string", "title": "Description explaining the purpose of this custom role" },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "List of permissions granted by this custom role"
         }
       },
+      "required": ["name","displayName","description","permissions"],
       "title": "Create custom role request"
     },

Based on server validations.


2507-2528: Align Update body schema with server validation (required + array caps).

Require displayName/description/permissions and cap permissions length; drop redundant type in items.

     "OrgUnitRoleUpdateCustomRoleBody": {
       "type": "object",
       "properties": {
         "displayName": { "type": "string", "title": "Updated display name for the custom role" },
         "description": { "type": "string", "title": "Updated description explaining the purpose of this custom role" },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "Updated list of permissions granted by this custom role"
         }
       },
+      "required": ["displayName","description","permissions"],
       "title": "Update custom role request"
     },

Based on server validations.


3607-3623: Constrain RolePermission: require fields and non-empty verbs.

Reflect server-side checks; ensure verbs are non-empty strings.

     "apiRolePermission": {
       "type": "object",
       "properties": {
         "resource": {
           "type": "string",
+          "minLength": 1,
           "title": "Resource name this permission applies to"
         },
         "verbs": {
           "type": "array",
+          "minItems": 1,
           "items": {
-            "type": "string"
+            "type": "string",
+            "minLength": 1
           },
           "title": "List of allowed verbs/actions for this resource"
         }
       },
+      "required": ["resource","verbs"],
       "title": "Permission definition for custom roles"
     },

Based on server validations.

🧹 Nitpick comments (13)
api/org-unit-role.proto (4)

65-68: Duplicate delete comment; keep a single clear summary.

Remove the redundant line.

-  // Delete a custom role from the organization unit
-  // Delete (soft delete) a custom role from the organization unit
+  // Delete (soft delete) a custom role from the organization unit

85-103: Prefer an enum for role type instead of free-form string.

Avoid typos and ease client validation by defining RoleType enum (e.g., BUILT_IN, CUSTOM) and use it in OrgUnitRolesListEntry.


148-151: Avoid pointer-from-slice trick for bool.

Use a named variable for clarity.

-    Active:      &[]bool{true}[0], // Mark role as active
+    Active:      func() *bool { b := true; return &b }(), // Mark role as active

56-63: Hard cap of 1000 roles; consider pagination.

List API currently truncates at 1000. Add pagination parameters to proto and implementation when feasible.

Also applies to: 75-78

api/swagger/apidocs.swagger.json (1)

301-303: Duplicate DELETE summary text; keep one line.

Remove the repeated sentence.

-        "summary": "Delete a custom role from the organization unit\nDelete (soft delete) a custom role from the organization unit",
+        "summary": "Delete (soft delete) a custom role from the organization unit",
pkg/server/org-unit-role.go (4)

148-151: Set Active bool pointer clearly.

Avoid slice literal hack for readability.

-    Active:      &[]bool{true}[0], // Mark role as active
+    Active:      func() *bool { b := true; return &b }(), // Mark role as active

56-63: List truncation: add pagination when feasible.

Hard-coded limit of 1000; expose offset/limit in proto and propagate to table.


299-314: Use contextual cancellation for handler registration.

Prefer ctx from GrpcServerContext (if available) instead of context.Background() to inherit server shutdown signals.


81-96: Server-side authorization posture.

Gateway enforces (api.role) permissions; direct gRPC access bypassing gateway would lack RBAC. Ensure gRPC is not publicly reachable or add server interceptors to enforce (api.role).

Can you confirm the gRPC port is internal-only (e.g., cluster-private, firewall restricted)? If not, I can propose a simple unary interceptor enforcing RBAC based on (api.role).

Also applies to: 168-178, 214-229, 246-268

pkg/server/org-unit-user.go (1)

141-158: Simplify pre-delete fetch if a keyed Find exists.

Prefer tbl.Find(ctx, key) for a single document lookup instead of building a bson filter + FindMany.

pkg/gateway/server.go (2)

236-248: Avoid double route matching for custom-role checks.

You already call matchRoute in ServeHTTP; pass RouteInfo into performOrgUnitRoleCheck/checkCustomRolePermissions to skip a second match.

Also applies to: 250-260, 262-279


224-234: Consider allowing CORS preflight (OPTIONS).

Currently only admin/auditor/custom checks run; preflight OPTIONS may be denied. Recommend bypassing RBAC for OPTIONS or treat as read-only.

pkg/table/org-unit-custom-role.go (1)

128-137: Consider a helper for bool pointers (optional).

The &[]bool{false}[0] idiom is valid but less readable. For improved clarity, consider a helper function like:

func boolPtr(b bool) *bool { return &b }

Then use: Active: boolPtr(false).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2180a27b45c00cfed51206017269178273707919 and 4bedf69ee178983e732e8e65acdbac08b3a110ac.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (5 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/table/const.go
  • main.go
  • pkg/gateway/routes.go
  • api/org-unit-role.pb.route.go
🧰 Additional context used
🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2496-2503: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (10)
pkg/server/org-unit-user.go (1)

172-195: Nice: binding-aware cleanup of soft-deleted custom roles.

Good resilience; logs on failure without blocking deletion.

pkg/gateway/server.go (2)

287-308: Good fix: extractRouteInfo now mirrors ServeHTTP RawPath handling.

Prevents encoded-path mismatches.


545-572: Wiring LGTM.

Custom role table added to gateway; error handling on init is consistent.

pkg/table/org-unit-custom-role.go (7)

18-56: LGTM! Well-structured data model with proper audit trails.

The data structures are well-designed with:

  • Composite key structure for proper scoping
  • Flexible permission model (resource + verbs)
  • Complete audit trail (Created, CreatedBy, Updated, UpdatedBy)
  • Three-state active field using *bool for soft-delete pattern (true/false/nil)

64-89: LGTM! Correct soft-delete filtering logic.

The filter "active": bson.M{"$ne": false} correctly implements the three-state soft-delete pattern, including roles where active is true or nil (not set), while excluding soft-deleted roles.


139-160: LGTM! Binding check logic is correct.

The method correctly checks for role assignments by querying the OrgUnitUserTable. Note that this creates coupling between tables through direct col field access, which is acceptable within the same package.


167-182: LGTM! Intelligent deletion strategy prevents data loss.

The method correctly implements a two-phase deletion strategy:

  • Soft delete when bindings exist (preserves referential integrity)
  • Permanent delete when no bindings (reclaims space)

This prevents orphaning user assignments while allowing efficient cleanup.


184-205: LGTM! Correct implementation for name conflict checks.

The method correctly finds roles regardless of active status, which is essential for preventing name conflicts with soft-deleted roles that still have bindings.


241-248: LGTM! Standard singleton pattern.

The getter correctly returns an error if the table hasn't been initialized, preventing nil pointer dereferences.


216-216: Confirm FindMany(limit=0) semantics Ensure that t.FindMany(ctx, filter, 0, 0) treats a 0 limit as “no limit” rather than returning zero results; otherwise this cleanup method never processes any roles.

Comment on lines +339 to +349
// Validate role name
if req.Name == "" {
return errors.New("role name cannot be empty")
}
if len(req.Name) < 2 {
return errors.New("role name must be at least 2 characters long")
}
if len(req.Name) > 50 {
return errors.New("role name cannot exceed 50 characters")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Enforce path-safe role names.

Add a charset check (letters/digits/hyphen/underscore/dot) to prevent routing issues with {name} path param.

   if len(req.Name) > 50 {
     return errors.New("role name cannot exceed 50 characters")
   }
+  if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+    return errors.New("role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate role name
if req.Name == "" {
return errors.New("role name cannot be empty")
}
if len(req.Name) < 2 {
return errors.New("role name must be at least 2 characters long")
}
if len(req.Name) > 50 {
return errors.New("role name cannot exceed 50 characters")
}
// Validate role name
if req.Name == "" {
return errors.New("role name cannot be empty")
}
if len(req.Name) < 2 {
return errors.New("role name must be at least 2 characters long")
}
if len(req.Name) > 50 {
return errors.New("role name cannot exceed 50 characters")
}
if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
return errors.New("role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
}
🤖 Prompt for AI Agents
In pkg/server/org-unit-role.go around lines 339 to 349, the role name validation
currently checks length only; add a path-safe charset check to ensure req.Name
contains only letters (A-Z, a-z), digits (0-9), hyphen (-), underscore (_), or
dot (.) — use a regex like `^[A-Za-z0-9._-]+$` to validate this and return an
explicit error (e.g., "role name contains invalid characters; only letters,
digits, hyphen, underscore and dot are allowed") when it fails; keep the
existing empty/length checks and perform this charset check after them.

Comment on lines +222 to +235
for _, role := range softDeletedRoles {
hasBindings, err := t.HasBindings(ctx, tenant, orgUnitId, role.Key.Name)
if err != nil {
continue // Skip on error, don't fail the entire cleanup
}

// If no bindings, permanently delete
if !hasBindings {
err = t.PermanentDelete(ctx, role.Key)
if err != nil {
// Log error but continue with other roles
continue
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add error logging and tracking for cleanup failures.

The cleanup loop silently swallows errors using continue, providing no visibility into failures. This could hide issues with binding checks or deletion operations.

Consider at minimum adding logging:

 	// Check each soft-deleted role for bindings
+	var failedRoles []string
 	for _, role := range softDeletedRoles {
 		hasBindings, err := t.HasBindings(ctx, tenant, orgUnitId, role.Key.Name)
 		if err != nil {
+			failedRoles = append(failedRoles, role.Key.Name)
+			// Log error: "Failed to check bindings for role %s: %v", role.Key.Name, err
 			continue // Skip on error, don't fail the entire cleanup
 		}
 
 		// If no bindings, permanently delete
 		if !hasBindings {
 			err = t.PermanentDelete(ctx, role.Key)
 			if err != nil {
+				failedRoles = append(failedRoles, role.Key.Name)
+				// Log error: "Failed to delete role %s: %v", role.Key.Name, err
 				// Log error but continue with other roles
 				continue
 			}
 		}
 	}
+	
+	if len(failedRoles) > 0 {
+		// Log warning: "Cleanup completed with %d failures for roles: %v", len(failedRoles), failedRoles
+	}
🤖 Prompt for AI Agents
In pkg/table/org-unit-custom-role.go around lines 222 to 235 the cleanup loop
currently swallows errors with continue; add error logging and basic failure
tracking so failures are visible and can be surfaced: when HasBindings returns
an error, log it with context (tenant, orgUnitId, role.Key.Name) and increment a
failure counter; likewise when PermanentDelete fails, log the deletion error
with the same context and increment the counter; after the loop, if the failure
counter is > 0, return or propagate an aggregated error indicating N cleanup
failures (or return the last error) so callers can react, otherwise continue to
succeed.

Comment on lines +250 to +269
// LocateOrgUnitCustomRoleTable initializes and returns the custom role table
func LocateOrgUnitCustomRoleTable(client db.StoreClient) (*OrgUnitCustomRoleTable, error) {
if orgUnitCustomRoleTable != nil {
return orgUnitCustomRoleTable, nil
}

col := client.GetCollection(AuthDatabaseName, OrgUnitCustomRoleCollectionName)
tbl := &OrgUnitCustomRoleTable{
col: col,
}

err := tbl.Initialize(col)
if err != nil {
return nil, err
}

orgUnitCustomRoleTable = tbl

return orgUnitCustomRoleTable, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add unique compound index on (key.tenant, key.orgUnitId, key.name)

As noted in the previous review, go-core-stack's Table.Initialize doesn't create DB indexes, and this repo contains no index-creation for this collection. Without a unique compound index on (tenant + orgUnitId + name), concurrent creates can insert duplicate roles, violating data integrity.

Based on learnings

Add the index in LocateOrgUnitCustomRoleTable during initialization or manage it via DB migrations/connection setup. For MongoDB, use the driver's CreateIndexes API with a unique compound index specification:

// After line 264, before setting orgUnitCustomRoleTable:
import "go.mongodb.org/mongo-driver/mongo"
import "go.mongodb.org/mongo-driver/mongo/options"

// Create unique compound index on (tenant, orgUnitId, name)
indexModel := mongo.IndexModel{
    Keys: bson.D{
        {Key: "key.tenant", Value: 1},
        {Key: "key.orgUnitId", Value: 1},
        {Key: "key.name", Value: 1},
    },
    Options: options.Index().SetUnique(true),
}

// Ensure index exists (idempotent)
if mongoCol, ok := col.(*mongo.Collection); ok {
    _, err = mongoCol.Indexes().CreateIndexes(ctx, []mongo.IndexModel{indexModel})
    if err != nil && !isIndexExistsError(err) {
        return nil, errors.Wrap(err, "failed to create unique index")
    }
}
🤖 Prompt for AI Agents
In pkg/table/org-unit-custom-role.go around lines 250 to 269, the
LocateOrgUnitCustomRoleTable initializer currently does not create a unique
compound index, allowing duplicate roles on concurrent inserts; add code during
initialization to create an idempotent unique compound index on key.tenant,
key.orgUnitId and key.name using the MongoDB driver's CreateIndexes API (build a
mongo.IndexModel with Keys for "key.tenant", "key.orgUnitId", "key.name" and
Options().SetUnique(true)), ensure you have a context, call
col.(*mongo.Collection).Indexes().CreateIndexes(ctx, []mongo.IndexModel{...})
only when the collection is a mongo.Collection, handle and return any non-
benign errors, and add the required mongo driver imports; keep the operation
idempotent and tolerate index-already-exists errors.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (4)
pkg/server/org-unit-role.go (1)

6-22: Enforce path-safe role names (letters/digits/._-).

Add a charset check for role names to prevent routing/storage issues. This was requested earlier.

Apply:

 import (
   "context"
-  "fmt"
+  "fmt"
   "log"
-  "time"
+  "time"
+  "regexp"
@@
 func (s *OrgUnitRoleServer) validateCreateCustomRoleRequest(req *api.CreateCustomRoleReq) error {
   // Validate role name
   if req.Name == "" {
     return errors.New("role name cannot be empty")
   }
   if len(req.Name) < 2 {
     return errors.New("role name must be at least 2 characters long")
   }
   if len(req.Name) > 50 {
     return errors.New("role name cannot exceed 50 characters")
   }
+  if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+    return errors.New("role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+  }

Also applies to: 339-351

api/swagger/apidocs.swagger.json (3)

2507-2532: Align CreateCustomRole request with server validation: required fields + cap permissions.
Add required fields, non-empty strings, and min/max items for permissions. Also avoid mixing "type" with "$ref" in items. This prevents client/server divergence and satisfies CKV_OPENAPI_21.

     "OrgUnitRoleCreateCustomRoleBody": {
       "type": "object",
       "properties": {
         "name": {
           "type": "string",
+          "minLength": 1,
           "title": "Name of the custom role (must be unique within the org unit)"
         },
         "displayName": {
           "type": "string",
+          "minLength": 1,
           "title": "Display name for the custom role"
         },
         "description": {
           "type": "string",
+          "minLength": 1,
           "title": "Description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "List of permissions granted by this custom role"
         }
       },
+      "required": ["name","displayName","description","permissions"],
       "title": "Create custom role request"
     },

2533-2554: Align UpdateCustomRole request with server validation: required fields + cap permissions.
Mark fields as required and enforce permissions limit; also remove redundant "type" with $ref.

     "OrgUnitRoleUpdateCustomRoleBody": {
       "type": "object",
       "properties": {
         "displayName": {
           "type": "string",
+          "minLength": 1,
           "title": "Updated display name for the custom role"
         },
         "description": {
           "type": "string",
+          "minLength": 1,
           "title": "Updated description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "Updated list of permissions granted by this custom role"
         }
       },
+      "required": ["displayName","description","permissions"],
       "title": "Update custom role request"
     },

3607-3627: Harden RolePermission schema: require fields, non-empty verbs, and enum action.
Match server validation by requiring resource and verbs, enforce non-empty, and constrain action to allowed values.

     "apiRolePermission": {
       "type": "object",
       "properties": {
         "resource": {
           "type": "string",
+          "minLength": 1,
           "title": "Resource name this permission applies to (supports \"*\" for all resources)"
         },
         "verbs": {
           "type": "array",
+          "minItems": 1,
           "items": {
-            "type": "string"
+            "type": "string",
+            "minLength": 1
           },
           "title": "List of allowed verbs/actions for this resource (supports \"*\" for all verbs)"
         },
         "action": {
-          "type": "string",
-          "title": "Action type: \"Allow\" or \"Deny\" (Deny takes precedence over Allow)"
+          "type": "string",
+          "enum": ["Allow","Deny"],
+          "default": "Allow",
+          "title": "Action type (Deny takes precedence over Allow)"
         }
       },
+      "required": ["resource","verbs"],
       "title": "Permission definition for custom roles"
     },
🧹 Nitpick comments (10)
pkg/server/org-unit-user.go (2)

172-195: Harden cleanup: log unexpected lookup errors for custom roles.

Errors other than not-found during FindAnyByNameAndOrgUnit are silently ignored. Log them for observability.

Apply:

-    existingRole, err := s.ouCustomRoleTbl.FindAnyByNameAndOrgUnit(ctx, authInfo.Realm, req.Ou, roleName)
-    if err == nil && existingRole.Active != nil && !*existingRole.Active {
+    existingRole, err := s.ouCustomRoleTbl.FindAnyByNameAndOrgUnit(ctx, authInfo.Realm, req.Ou, roleName)
+    if err == nil && existingRole.Active != nil && !*existingRole.Active {
       ...
-    }
+    } else if err != nil && !errors.IsNotFound(err) {
+      log.Printf("failed to lookup custom role %s for cleanup: %s", roleName, err)
+    }

244-261: LGTM: validateRole distinguishes built-in vs custom.

Optional: centralize built-in roles in a shared set to avoid duplication across gateway and servers.

pkg/gateway/server.go (1)

262-328: LGTM: Deny precedence with wildcard support implemented.

Optional: pass already-computed route info from ServeHTTP to avoid a second route match per request.

pkg/server/org-unit-role.go (4)

169-178: Validate role name in Update as well.

Add the same charset check for req.Name to guard path param misuse.

Apply:

 func (s *OrgUnitRoleServer) UpdateCustomRole(ctx context.Context, req *api.UpdateCustomRoleReq) (*api.UpdateCustomRoleResp, error) {
   authInfo, _ := auth.GetAuthInfoFromContext(ctx)
   if authInfo == nil {
     return nil, status.Errorf(codes.Unauthenticated, "User not authenticated")
   }
 
   // Validate request fields
   if err := s.validateUpdateCustomRoleRequest(req); err != nil {
     return nil, status.Errorf(codes.InvalidArgument, "Invalid request: %s", err)
   }
+  if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+    return nil, status.Errorf(codes.InvalidArgument, "role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+  }

215-219: Validate role name in Get.

Apply:

 func (s *OrgUnitRoleServer) GetCustomRole(ctx context.Context, req *api.GetCustomRoleReq) (*api.GetCustomRoleResp, error) {
   authInfo, _ := auth.GetAuthInfoFromContext(ctx)
   if authInfo == nil {
     return nil, status.Errorf(codes.Unauthenticated, "User not authenticated")
   }
+  if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+    return nil, status.Errorf(codes.InvalidArgument, "role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+  }

247-252: Validate role name in Delete.

Apply:

 func (s *OrgUnitRoleServer) DeleteCustomRole(ctx context.Context, req *api.DeleteCustomRoleReq) (*api.DeleteCustomRoleResp, error) {
   authInfo, _ := auth.GetAuthInfoFromContext(ctx)
   if authInfo == nil {
     return nil, status.Errorf(codes.Unauthenticated, "User not authenticated")
   }
+  if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+    return nil, status.Errorf(codes.InvalidArgument, "role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+  }

339-396: LGTM: request validation (lengths, permissions).

Consider adding a per-permission verbs limit and duplicate elimination (optional).

api/org-unit-role.proto (1)

115-125: Prefer enum for action over string.

Using an enum prevents typos and eases validation (“Allow”/“Deny”).

Example:

enum Action {
  ACTION_UNSPECIFIED = 0;
  ALLOW = 1;
  DENY = 2;
}
message RolePermission {
  string resource = 1;
  repeated string verbs = 2;
  Action action = 3;
}
api/swagger/apidocs.swagger.json (2)

302-303: Deduplicate and clarify DELETE summary.
Summary repeats itself and mentions “soft delete”; keep concise in summary, move details to description.

-        "summary": "Delete a custom role from the organization unit\nDelete (soft delete) a custom role from the organization unit",
+        "summary": "Delete (soft delete) a custom role from the organization unit",
+        "description": "Soft-deletes a custom role within the specified organization unit.",

2833-2876: Fix permissions.items to avoid mixing $ref with type.
OpenAPI 2.0 recommends using $ref without an extra “type”. Remove the redundant "type".

         "permissions": {
           "type": "array",
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "List of permissions granted by this custom role"
         },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 4bedf69ee178983e732e8e65acdbac08b3a110ac and 043d902e2e320c709e696c9eef815a52b0daa3ef.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (5 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • main.go
  • pkg/table/org-unit-custom-role.go
  • pkg/gateway/routes.go
  • api/org-unit-role.pb.route.go
🧰 Additional context used
🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2522-2529: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (22)
pkg/table/const.go (1)

30-32: LGTM: collection name added consistently.

pkg/server/org-unit-user.go (2)

26-28: LGTM: custom role table wired into server.


206-215: LGTM: custom role table retrieval added to constructor.

pkg/gateway/server.go (6)

47-56: LGTM: gateway wired with custom role table and proxies.


250-260: LGTM: route-derived permission check for custom roles.


330-357: LGTM: extractRouteInfo mirrors ServeHTTP path selection.


359-377: LGTM: unified RawPath/Path handling in ServeHTTP.


594-598: LGTM: custom role table retrieval.


613-621: LGTM: gateway fields initialized with custom role table.

pkg/server/org-unit-role.go (4)

26-27: LGTM: server holds custom role table.


32-55: LGTM: list returns built-in plus custom roles; non-fatal on fetch error.

Also applies to: 56-78


301-311: LGTM: server wiring and handler/route registration.


398-444: LGTM: update validation mirrors create (sans name).

api/org-unit-role.proto (3)

27-38: LGTM: CRUD RPCs with HTTP mappings and api.role metadata.

Also applies to: 40-51, 53-63


92-103: LGTM: list entry enriched for custom roles.


27-38: Route resource and verb definitions align with gateway registrations. The resource string "org-unit-custom-role" and verbs ("create", "update", "get", "delete") in api/org-unit-role.proto match the entries in api.RoutesOrgUnitRole.

api/swagger/apidocs.swagger.json (6)

223-262: New create endpoint looks correct and consistent.
POST path, params, tags, and response wiring align with the rest of the spec.


386-386: Updated list summary reads well.
Clear that built‑in and custom roles are included.


2706-2715: Create response schema is fine.
Simple confirmation message matches spec style elsewhere.


2823-2832: Delete response schema is fine.
Consistent with simple “message” response pattern.


3418-3437: List entry extensions look good.
The added fields (type/displayName/created/createdBy) improve list context without breaking existing clients.


4065-4074: Update response schema is fine.
Matches the simple “message” pattern used elsewhere.

Comment thread api/org-unit-role.proto Outdated
Comment on lines +65 to +76
// Delete a custom role from the organization unit
// Delete (soft delete) a custom role from the organization unit
rpc DeleteCustomRole(DeleteCustomRoleReq) returns (DeleteCustomRoleResp) {
option (google.api.http) = {
delete: "/api/auth/v1/ou/{ou}/role/{name}"
};
option (api.role) = {
resource: "org-unit-custom-role"
scope: "ou"
verb: "delete"
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate comment line.

There are two comment lines for Delete; keep the soft-delete note once.

Apply:

-  // Delete a custom role from the organization unit
   // Delete (soft delete) a custom role from the organization unit
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Delete a custom role from the organization unit
// Delete (soft delete) a custom role from the organization unit
rpc DeleteCustomRole(DeleteCustomRoleReq) returns (DeleteCustomRoleResp) {
option (google.api.http) = {
delete: "/api/auth/v1/ou/{ou}/role/{name}"
};
option (api.role) = {
resource: "org-unit-custom-role"
scope: "ou"
verb: "delete"
};
}
// Delete (soft delete) a custom role from the organization unit
rpc DeleteCustomRole(DeleteCustomRoleReq) returns (DeleteCustomRoleResp) {
option (google.api.http) = {
delete: "/api/auth/v1/ou/{ou}/role/{name}"
};
option (api.role) = {
resource: "org-unit-custom-role"
scope: "ou"
verb: "delete"
};
}
🤖 Prompt for AI Agents
In api/org-unit-role.proto around lines 65 to 76, the RPC DeleteCustomRole has
duplicate leading comments; remove the redundant "Delete a custom role from the
organization unit" line and keep the single soft-delete description "Delete
(soft delete) a custom role from the organization unit" so only the clear
soft-delete note remains above the rpc definition.

Comment thread pkg/gateway/server.go
Comment on lines +236 to +248
// Check if it's a custom role
customRole, err := s.ouCustomRoleTbl.FindByNameAndOrgUnit(r.Context(), authInfo.Realm, ou, ouUser.Role)
if err != nil {
if !errors.IsNotFound(err) {
log.Printf("failed to find custom role %s for org unit %s: %s", ouUser.Role, ou, err)
}
// If custom role not found, deny access
return false
}

// Check if the custom role allows the requested action
return s.checkCustomRolePermissions(customRole, r)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle built-in "default" role explicitly; avoid accidental deny.

With no "default" case, flow falls into custom-role lookup for role "default" and denies access. Add an explicit case with intended semantics (commonly “no special OU privileges”).

Apply:

 switch ouUser.Role {
 case "admin":
   return true
 case "auditor":
   if r.Method == http.MethodGet || r.Method == http.MethodHead {
     return true
   }
   return false
+case "default":
+  // No special OU privileges; handled as least-privileged
+  return false
 }

Optional: also allow OPTIONS for read-only roles to avoid CORS/preflight failures:

- if r.Method == http.MethodGet || r.Method == http.MethodHead {
+ if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pkg/gateway/server.go around lines 236 to 248, the code currently falls
through to a custom-role lookup for the built-in "default" role and ends up
denying access when no custom role exists; add an explicit branch that checks if
ouUser.Role == "default" and return the intended semantics (commonly: treat as
"no special OU privileges" by returning true so it does not deny access here),
placing this check before the FindByNameAndOrgUnit call; optionally also allow
HTTP OPTIONS through for read-only roles to avoid CORS/preflight failures by
checking r.Method == "OPTIONS" and returning true for those roles.

Comment thread pkg/server/org-unit-user.go
Comment on lines +141 to +158
// Get the user data before deletion to check their role
filter := bson.M{
"key.tenant": authInfo.Realm,
"key.username": req.User,
"key.orgUnitId": req.Ou,
}
users, err := s.tbl.FindMany(ctx, filter, 0, 1)
if err != nil {
log.Printf("failed to get org unit user before deletion, got error: %s", err)
return nil, status.Errorf(codes.Internal, "Something went wrong, please try again later")
}

if len(users) == 0 {
return nil, status.Errorf(codes.NotFound, "Org Unit User %s, not found", req.User)
}

userData := users[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Map NotFound correctly when pre-fetching user for deletion.

If FindMany returns a not-found error for zero results, current code returns Internal. Handle NotFound explicitly.

Apply:

  users, err := s.tbl.FindMany(ctx, filter, 0, 1)
- if err != nil {
-   log.Printf("failed to get org unit user before deletion, got error: %s", err)
-   return nil, status.Errorf(codes.Internal, "Something went wrong, please try again later")
- }
+ if err != nil {
+   if errors.IsNotFound(err) {
+     return nil, status.Errorf(codes.NotFound, "Org Unit User %s, not found", req.User)
+   }
+   log.Printf("failed to get org unit user before deletion, got error: %s", err)
+   return nil, status.Errorf(codes.Internal, "Something went wrong, please try again later")
+ }
 
 if len(users) == 0 {
   return nil, status.Errorf(codes.NotFound, "Org Unit User %s, not found", req.User)
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pkg/server/org-unit-user.go around lines 141 to 158, the call to
s.tbl.FindMany treats any error as Internal; modify the error handling to detect
the repository's "not found" sentinel (e.g. errors.Is(err, store.ErrNotFound) or
mongo.ErrNoDocuments depending on the table implementation) and return
status.Errorf(codes.NotFound, ...) when that is the case, otherwise keep
returning the Internal error; keep the existing zero-length users check for
safety.

@Suryanshu185 Suryanshu185 force-pushed the main branch 2 times, most recently from 3d3c936 to 1be6b36 Compare October 22, 2025 05:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/server/org-unit-user.go (1)

61-108: AddOrgUnitUser still blocks custom roles.
We now have validateRole, yet AddOrgUnitUser keeps the hard-coded “admin/default/auditor” check. That means newly created custom roles still can’t be assigned on creation — the core feature stalls at step zero. Please reuse validateRole here just like Update does:

@@
-	// validate role, currently only admin, default and auditor roles are allowed
-	if req.Role != "admin" && req.Role != "default" && req.Role != "auditor" {
-		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
-	}
+	if err := s.validateRole(ctx, req.Role, req.Ou, authInfo.Realm); err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s - %v", req.Role, err)
+	}
♻️ Duplicate comments (4)
pkg/server/org-unit-role.go (1)

357-368: Validate role names are path-safe.
Role names feed directly into {name} URL segments. Without constraining them, names containing /, %2F, spaces, etc. break the CRUD endpoints. Please add the simple charset guard (letters / digits / ._-) as previously discussed:

 	if len(req.Name) < 2 {
 		return errors.New("role name must be at least 2 characters long")
 	}
 	if len(req.Name) > 50 {
 		return errors.New("role name cannot exceed 50 characters")
 	}
+	if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+		return errors.New("role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+	}

(Remember to import regexp.)

api/org-unit-role.proto (1)

65-67: Remove the duplicated Delete comment line.
The two consecutive comments above DeleteCustomRole are redundant; keep the soft-delete explanation once to avoid OpenAPI/doc bloat.

pkg/table/org-unit-custom-role.go (1)

268-280: Still missing the unique compound index guard.
We again have no protection against concurrent duplicates: LocateOrgUnitCustomRoleTable never creates a unique index on (key.tenant, key.orgUnitId, key.name), so two fast creates can persist conflicting roles. Please ensure this index is created during initialization (or via your migrations) before exposing the table; otherwise data integrity is at risk. A minimal fix is shown below—adjust to match your index helpers:

@@
-import (
-	"context"
-	"time"
-
-	"github.com/go-core-stack/core/db"
-	"github.com/go-core-stack/core/errors"
-	"github.com/go-core-stack/core/table"
-	"go.mongodb.org/mongo-driver/bson"
-)
+import (
+	"context"
+	"strings"
+	"time"
+
+	"github.com/go-core-stack/core/db"
+	"github.com/go-core-stack/core/errors"
+	"github.com/go-core-stack/core/table"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
@@
 func LocateOrgUnitCustomRoleTable(client db.StoreClient) (*OrgUnitCustomRoleTable, error) {
@@
-	col := client.GetCollection(AuthDatabaseName, OrgUnitCustomRoleCollectionName)
+	col := client.GetCollection(AuthDatabaseName, OrgUnitCustomRoleCollectionName)
@@
-	err := tbl.Initialize(col)
+	err := tbl.Initialize(col)
 	if err != nil {
 		return nil, err
 	}
+
+	if mongoCol, ok := col.(*mongo.Collection); ok {
+		indexModel := mongo.IndexModel{
+			Keys: bson.D{
+				{Key: "key.tenant", Value: 1},
+				{Key: "key.orgUnitId", Value: 1},
+				{Key: "key.name", Value: 1},
+			},
+			Options: options.Index().SetUnique(true),
+		}
+		if _, idxErr := mongoCol.Indexes().CreateOne(context.Background(), indexModel); idxErr != nil && !strings.Contains(idxErr.Error(), "already exists") {
+			return nil, errors.Wrap(idxErr, "ensure org-unit custom role unique index")
+		}
+	}
pkg/server/org-unit-user.go (1)

147-155: Surface not-found instead of masking it as internal.
If FindMany returns NotFound (common when the user never existed), we now log it and bubble up codes.Internal, which is the same gap noted earlier. Please restore the original behaviour by mapping that sentinel to codes.NotFound before returning:

 	users, err := s.tbl.FindMany(ctx, filter, 0, 1)
 	if err != nil {
+		if errors.IsNotFound(err) {
+			return nil, status.Errorf(codes.NotFound, "Org Unit User %s, not found", req.User)
+		}
 		log.Printf("failed to get org unit user before deletion, got error: %s", err)
 		return nil, status.Errorf(codes.Internal, "Something went wrong, please try again later")
 	}
🧹 Nitpick comments (3)
pkg/server/org-unit-user.go (1)

244-261: Expose validateRole for both Add & Update paths.
Now that we introduced validateRole, make sure every entry point (Add as well as Update/Delete flows) calls it. See comment above for wiring it into AddOrgUnitUser so custom roles can actually be assigned during creation.

api/swagger/apidocs.swagger.json (2)

2508-2554: Swagger schema is looser than server validation.
The OpenAPI still treats all custom-role fields as optional with unbounded permissions, but the server rejects empty name/displayName/description/permissions and caps permissions at 20. Bring the contract in sync:

     "OrgUnitRoleCreateCustomRoleBody": {
       "type": "object",
       "properties": {
         "name": {
-          "type": "string",
+          "type": "string",
+          "minLength": 1,
           "title": "Name of the custom role (must be unique within the org unit)"
         },
         "displayName": {
-          "type": "string",
+          "type": "string",
+          "minLength": 1,
           "title": "Display name for the custom role"
         },
         "description": {
-          "type": "string",
+          "type": "string",
+          "minLength": 1,
           "title": "Description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "List of permissions granted by this custom role"
         }
       },
+      "required": ["name", "displayName", "description", "permissions"],
       "title": "Create custom role request"
     },
@@
     "OrgUnitRoleUpdateCustomRoleBody": {
       "type": "object",
       "properties": {
         "displayName": {
-          "type": "string",
+          "type": "string",
+          "minLength": 1,
           "title": "Updated display name for the custom role"
         },
         "description": {
-          "type": "string",
+          "type": "string",
+          "minLength": 1,
           "title": "Updated description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
-            "type": "object",
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "Updated list of permissions granted by this custom role"
         }
       },
+      "required": ["displayName", "description", "permissions"],
       "title": "Update custom role request"
     },

3608-3645: Mark required fields on apiRolePermission.
Server-side validation insists on non-empty resource and at least one verb, but the schema leaves them optional. Please tighten the definition:

     "apiRolePermission": {
       "type": "object",
       "properties": {
         "resource": {
-          "type": "string",
+          "type": "string",
+          "minLength": 1,
           "title": "Resource name this permission applies to"
         },
         "verbs": {
           "type": "array",
+          "minItems": 1,
           "items": {
-            "type": "string"
+            "type": "string",
+            "minLength": 1
           },
           "title": "List of allowed verbs/actions for this resource (supports \"*\" for all verbs)"
         }
       },
+      "required": ["resource", "verbs"],
       "title": "Permission definition for custom roles"
     },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 043d902e2e320c709e696c9eef815a52b0daa3ef and 1be6b361a88f54cf0641ab9034d2f577dc9de832.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (8 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/org-unit-role.pb.route.go
  • pkg/table/const.go
  • main.go
🧰 Additional context used
🪛 ast-grep (0.39.6)
pkg/gateway/server.go

[warning] 351-351: MinVersionis missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{InsecureSkipVerify: true}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2522-2529: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🪛 GitHub Actions: Validate build for pull request
pkg/gateway/server.go

[error] 141-141: golangci-lint: Error return value of io.Copy is not checked. (errcheck)

🪛 GitHub Check: build-pull-request
pkg/gateway/server.go

[failure] 361-361:
Error return value of resp.Body.Close is not checked (errcheck)


[failure] 170-170:
Error return value of w.ResponseWriter.Write is not checked (errcheck)


[failure] 163-163:
Error return value of io.Copy is not checked (errcheck)


[failure] 151-151:
Error return value of io.Copy is not checked (errcheck)


[failure] 141-141:
Error return value of io.Copy is not checked (errcheck)


[failure] 681-681:
QF1003: could use tagged switch on action (staticcheck)


[failure] 1065-1065:
ST1023: should omit type http.ResponseWriter from declaration; it will be inferred from the right-hand side (staticcheck)

Comment thread pkg/gateway/server.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
pkg/server/org-unit-user.go (2)

67-70: Let AddOrgUnitUser accept custom roles.

AddOrgUnitUser still rejects every non built-in role, so callers cannot assign the custom roles this feature introduces. Reuse validateRole here just like the update path.

-	// validate role, currently only admin, default and auditor roles are allowed
-	if req.Role != "admin" && req.Role != "default" && req.Role != "auditor" {
-		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
-	}
+	// validate role - allow built-in or existing custom role
+	if err := s.validateRole(ctx, req.Role, req.Ou, authInfo.Realm); err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s - %v", req.Role, err)
+	}

148-156: Return NotFound when the org-unit user is missing.

FindMany can surface the repo’s not-found sentinel. Right now we convert that into an Internal error, hiding the 404 from clients. Handle errors.IsNotFound before logging so the delete RPC returns the correct NotFound status.

	users, err := s.tbl.FindMany(ctx, filter, 0, 1)
	if err != nil {
+		if errors.IsNotFound(err) {
+			return nil, status.Errorf(codes.NotFound, "Org Unit User %s, not found", req.User)
+		}
 		log.Printf("failed to get org unit user before deletion, got error: %s", err)
 		return nil, status.Errorf(codes.Internal, "Something went wrong, please try again later")
	}
pkg/table/org-unit-custom-role.go (1)

6-14: Enforce a unique index for (tenant, orgUnitId, name).

Concurrent creates can still write duplicate roles because we never ensure a unique compound index on the collection. Please create the (key.tenant, key.orgUnitId, key.name) index during table location, tolerating “already exists” responses.

-import (
-	"context"
-	"time"
-
-	"github.com/go-core-stack/core/db"
-	"github.com/go-core-stack/core/errors"
-	"github.com/go-core-stack/core/table"
-	"go.mongodb.org/mongo-driver/bson"
-)
+import (
+	stdErrors "errors"
+	"context"
+	"time"
+
+	"github.com/go-core-stack/core/db"
+	"github.com/go-core-stack/core/errors"
+	"github.com/go-core-stack/core/table"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
@@
 func LocateOrgUnitCustomRoleTable(client db.StoreClient) (*OrgUnitCustomRoleTable, error) {
 	if orgUnitCustomRoleTable != nil {
 		return orgUnitCustomRoleTable, nil
 	}
 
 	col := client.GetCollection(AuthDatabaseName, OrgUnitCustomRoleCollectionName)
@@
-	err := tbl.Initialize(col)
+	err := tbl.Initialize(col)
 	if err != nil {
 		return nil, err
 	}
 
+	if mongoCol, ok := col.(*mongo.Collection); ok {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+
+		indexModel := mongo.IndexModel{
+			Keys: bson.D{
+				{Key: "key.tenant", Value: 1},
+				{Key: "key.orgUnitId", Value: 1},
+				{Key: "key.name", Value: 1},
+			},
+			Options: options.Index().SetUnique(true),
+		}
+
+		if _, err := mongoCol.Indexes().CreateOne(ctx, indexModel); err != nil {
+			var cmdErr mongo.CommandError
+			if !stdErrors.As(err, &cmdErr) || cmdErr.Code != 85 {
+				return nil, errors.Wrapf(err, "failed to ensure unique index on %s", OrgUnitCustomRoleCollectionName)
+			}
+		}
+	}
+
 	orgUnitCustomRoleTable = tbl
 
 	return orgUnitCustomRoleTable, nil
 }

Also applies to: 262-280

pkg/gateway/server.go (1)

137-170: Handle response write errors in finalize.

golangci-lint (errcheck) is failing because we drop the results from io.Copy and Write. More importantly, we would never know if streaming the response fails. Please capture and log these errors.

 	if !w.shouldFilter || w.statusCode != http.StatusOK || w.body.Len() == 0 {
 		// If Nothing to filter
 		if w.statusCode != http.StatusOK && w.body.Len() > 0 {
 			w.ResponseWriter.WriteHeader(w.statusCode)
-			io.Copy(w.ResponseWriter, w.body)
+			if _, err := io.Copy(w.ResponseWriter, w.body); err != nil {
+				log.Printf("failed to stream original response body: %v", err)
+			}
 		}
 		return
 	}
@@
 	if err := json.Unmarshal(w.body.Bytes(), &data); err != nil {
 		// If Not JSON or parse error ---> write original response
 		w.ResponseWriter.WriteHeader(w.statusCode)
-		io.Copy(w.ResponseWriter, w.body)
+		if _, copyErr := io.Copy(w.ResponseWriter, w.body); copyErr != nil {
+			log.Printf("failed to stream original response body after unmarshal error: %v", copyErr)
+		}
 		return
 	}
@@
 	if err != nil {
 		// Marshal error ---> write original response
 		w.ResponseWriter.WriteHeader(w.statusCode)
-		io.Copy(w.ResponseWriter, w.body)
+		if _, copyErr := io.Copy(w.ResponseWriter, w.body); copyErr != nil {
+			log.Printf("failed to stream original response body after marshal error: %v", copyErr)
+		}
 		return
 	}
@@
 	w.ResponseWriter.Header().Set("Content-Length", fmt.Sprintf("%d", len(filteredBytes)))
 	w.ResponseWriter.WriteHeader(w.statusCode)
-	w.ResponseWriter.Write(filteredBytes)
+	if _, writeErr := w.ResponseWriter.Write(filteredBytes); writeErr != nil {
+		log.Printf("failed to write filtered response body: %v", writeErr)
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 1be6b361a88f54cf0641ab9034d2f577dc9de832 and 99d34374a3d9851ac34ae59aafeded923d1b4ca3.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (8 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • main.go
  • pkg/gateway/routes.go
  • api/org-unit-role.pb.route.go
🧰 Additional context used
🪛 ast-grep (0.39.6)
pkg/gateway/server.go

[warning] 360-360: MinVersionis missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{InsecureSkipVerify: true}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2522-2529: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🪛 GitHub Actions: Validate build for pull request
pkg/gateway/server.go

[error] 141-141: golangci-lint: Error return value of io.Copy is not checked (errcheck)

🪛 GitHub Check: build-pull-request
pkg/gateway/server.go

[failure] 370-370:
Error return value of resp.Body.Close is not checked (errcheck)


[failure] 170-170:
Error return value of w.ResponseWriter.Write is not checked (errcheck)


[failure] 163-163:
Error return value of io.Copy is not checked (errcheck)


[failure] 151-151:
Error return value of io.Copy is not checked (errcheck)


[failure] 141-141:
Error return value of io.Copy is not checked (errcheck)


[failure] 690-690:
QF1003: could use tagged switch on action (staticcheck)


[failure] 1074-1074:
ST1023: should omit type http.ResponseWriter from declaration; it will be inferred from the right-hand side (staticcheck)

Comment thread api/swagger/apidocs.swagger.json
@Suryanshu185 Suryanshu185 force-pushed the main branch 4 times, most recently from 01ede3d to 5cb1ae2 Compare October 22, 2025 06:08
@Suryanshu185 Suryanshu185 force-pushed the main branch 2 times, most recently from 2a4e808 to 44084ee Compare October 22, 2025 06:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/server/org-unit-user.go (1)

67-70: Call validateRole so Add honours custom roles

Add still hard-codes the old built-in check, so any org-unit custom role is rejected at creation time. That breaks the CRUD story introduced in this PR. Route Add through validateRole exactly like Update so both built-ins and existing custom roles work.

Apply this diff:

-	// validate role, currently only admin, default and auditor roles are allowed
-	if req.Role != "admin" && req.Role != "default" && req.Role != "auditor" {
-		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
-	}
+	// validate role - allow built-in roles or existing custom roles
+	if err := s.validateRole(ctx, req.Role, req.Ou, authInfo.Realm); err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s - %v", req.Role, err)
+	}
♻️ Duplicate comments (1)
api/swagger/apidocs.swagger.json (1)

2508-2554: Sync schema validation with server-side rules

The new CRUD handlers reject blank names/display names/descriptions and enforce 1–20 permissions, each with non-empty verbs/resources. The OpenAPI schemas still mark these fields optional and unconstrained, so generated clients will happily send payloads the server refuses. Please mirror the server validation (see validateCreateCustomRoleRequest / validateUpdateCustomRoleRequest) by adding minLength, minItems/maxItems, and the appropriate required lists.

Suggested adjustments (abridged):

         "name": {
           "type": "string",
+          "minLength": 1,
           "title": "Name of the custom role (must be unique within the org unit)"
         },
         "displayName": {
           "type": "string",
+          "minLength": 1,
           "title": "Display name for the custom role"
         },
         "description": {
           "type": "string",
+          "minLength": 1,
           "title": "Description explaining the purpose of this custom role"
         },
         "permissions": {
           "type": "array",
+          "minItems": 1,
+          "maxItems": 20,
           "items": {
             "$ref": "#/definitions/apiRolePermission"
           },
           "title": "List of permissions granted by this custom role"
         }
       },
+      "required": ["name","displayName","description","permissions"],

Apply the same treatment to OrgUnitRoleUpdateCustomRoleBody, and in apiRolePermission add:

 "resource": { "type": "string", "minLength": 1, ... },
 "verbs": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
 "required": ["resource","verbs"]

This keeps the documented contract in lockstep with the enforced business rules.

🧹 Nitpick comments (1)
pkg/server/org-unit-role.go (1)

357-411: Tighten role-name validation to path-safe characters

Role names travel in the {name} path segment for the REST gateway. Without constraining the charset, names containing /, %, or whitespace can break routing or produce surprising encodings. Add a simple regexp (letters, digits, _, -, .) after the length checks so we reject unsafe names up front.

Example adjustment:

 	if len(req.Name) > 50 {
 		return errors.New("role name cannot exceed 50 characters")
 	}
+	if ok, _ := regexp.MatchString(`^[A-Za-z0-9._-]+$`, req.Name); !ok {
+		return errors.New("role name may contain only letters, digits, hyphen (-), underscore (_), and dot (.)")
+	}

Be sure to add the same guard inside validateUpdateCustomRoleRequest for symmetry and import regexp.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between e7b8fa3d611d3c1af3caab11cf617ef0e62d8ca1 and 44084ee209f7b25f4671aa2dc4e7615037b74e87.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (8 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • main.go
  • pkg/table/const.go
  • api/org-unit-role.pb.route.go
🧰 Additional context used
🪛 ast-grep (0.39.6)
pkg/gateway/server.go

[warning] 362-362: MinVersionis missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{InsecureSkipVerify: true}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2522-2529: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

Comment on lines +37 to 54
items := []*api.OrgUnitRolesListEntry{
{
Name: "admin",
Desc: "Administrator role to provide access to everything in the Organization Unit including management of users and resources",
Type: "built-in",
DisplayName: "Administrator",
Created: 0,
CreatedBy: "",
},
{
Name: "auditor",
Desc: "Auditor role to provide read-only access to all the resources available in the Organization Unit",
Type: "built-in",
DisplayName: "Auditor",
Created: 0,
CreatedBy: "",
},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return the built-in default role too

ListOrgUnitRoles only emits admin and auditor, but default is still treated as a built-in everywhere else (e.g. Add validates it, Delete skips cleanup for it). Omitting it here means clients pulling this list will never discover default, so they cannot assign or manage users with that baseline role. Please add the default definition alongside the other built-ins.

Suggested addition:

 		{
 			Name:        "auditor",
 			Desc:        "Auditor role to provide read-only access to all the resources available in the Organization Unit",
 			Type:        "built-in",
 			DisplayName: "Auditor",
 			Created:     0,
 			CreatedBy:   "",
 		},
+		{
+			Name:        "default",
+			Desc:        "Baseline org unit role with no special privileges.",
+			Type:        "built-in",
+			DisplayName: "Default",
+			Created:     0,
+			CreatedBy:   "",
+		},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
items := []*api.OrgUnitRolesListEntry{
{
Name: "admin",
Desc: "Administrator role to provide access to everything in the Organization Unit including management of users and resources",
Type: "built-in",
DisplayName: "Administrator",
Created: 0,
CreatedBy: "",
},
{
Name: "auditor",
Desc: "Auditor role to provide read-only access to all the resources available in the Organization Unit",
Type: "built-in",
DisplayName: "Auditor",
Created: 0,
CreatedBy: "",
},
}
items := []*api.OrgUnitRolesListEntry{
{
Name: "admin",
Desc: "Administrator role to provide access to everything in the Organization Unit including management of users and resources",
Type: "built-in",
DisplayName: "Administrator",
Created: 0,
CreatedBy: "",
},
{
Name: "auditor",
Desc: "Auditor role to provide read-only access to all the resources available in the Organization Unit",
Type: "built-in",
DisplayName: "Auditor",
Created: 0,
CreatedBy: "",
},
{
Name: "default",
Desc: "Baseline org unit role with no special privileges.",
Type: "built-in",
DisplayName: "Default",
Created: 0,
CreatedBy: "",
},
}
🤖 Prompt for AI Agents
In pkg/server/org-unit-role.go around lines 37 to 54, the built-in "default"
role is missing from the items slice returned by ListOrgUnitRoles; add a third
entry for the "default" role with Name: "default", Desc: a short description
(e.g., baseline/default role for new users), Type: "built-in", DisplayName:
"Default", and Created/CreatedBy set consistently with the other built-ins (0
and empty string) so clients can discover and assign the baseline role.

…d permission checks

Introduce complete support for custom roles scoped to an organization unit in the auth‑gateway.
Includes the ability to create, update, retrieve, delete, and list roles within their org‑unit boundaries.

Signed-off-by: suryanshu185 <suryanshu.gupta@kluisz.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/server/org-unit-user.go (1)

67-70: Let Add reuse validateRole so custom roles work.

AddOrgUnitUser still hard-codes the three built-ins, so any custom role created by this feature is rejected at creation time. UpdateOrgUnitUser already delegates to validateRole; do the same here so we honor built-ins and existing custom roles. Without this change, the new custom-role CRUD is unusable for new assignments.

Apply this diff:

-	// validate role, currently only admin, default and auditor roles are allowed
-	if req.Role != "admin" && req.Role != "default" && req.Role != "auditor" {
-		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s", req.Role)
-	}
+	// validate role - allow built-in or existing custom role
+	if err := s.validateRole(ctx, req.Role, req.Ou, authInfo.Realm); err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "Invalid role: %s - %v", req.Role, err)
+	}
♻️ Duplicate comments (5)
pkg/gateway/server.go (1)

634-654: Handle the built-in default role explicitly.

When an OU user has the built-in default role we immediately fall through to the custom-role lookup, which fails and denies access. We already treat default as a first-class built-in elsewhere, so add an explicit branch so baseline users aren’t locked out.

 	switch ouUser.Role {
 	case "admin":
 		// wildcard access to the org unit
 		return true
 	case "auditor":
 		if r.Method == http.MethodGet || r.Method == http.MethodHead {
 			// allow read-only access for auditor role
 			return true
 		}
 		return false
+	case "default":
+		// Baseline role: no elevated privileges beyond general tenant checks
+		return false
 	}
pkg/server/org-unit-role.go (1)

37-54: Include the built-in default role in the list response.

The List RPC still only emits admin and auditor. We rely on the default role elsewhere (validation, cleanup), so leaving it out means clients never discover or assign the baseline role. Re-add the default entry alongside the other built-ins.

 	items := []*api.OrgUnitRolesListEntry{
 		{
 			Name:        "admin",
 			Desc:        "Administrator role to provide access to everything in the Organization Unit including management of users and resources",
 			Type:        "built-in",
 			DisplayName: "Administrator",
 			Created:     0,
 			CreatedBy:   "",
 		},
 		{
 			Name:        "auditor",
 			Desc:        "Auditor role to provide read-only access to all the resources available in the Organization Unit",
 			Type:        "built-in",
 			DisplayName: "Auditor",
 			Created:     0,
 			CreatedBy:   "",
 		},
+		{
+			Name:        "default",
+			Desc:        "Baseline org unit role with no special privileges.",
+			Type:        "built-in",
+			DisplayName: "Default",
+			Created:     0,
+			CreatedBy:   "",
+		},
 	}
pkg/table/org-unit-custom-role.go (1)

262-280: Add the unique (tenant, orgUnitId, name) index during table init.

We still never create a compound unique index on (key.tenant, key.orgUnitId, key.name), so concurrent creates can insert duplicates. Please set up the index inside LocateOrgUnitCustomRoleTable (or equivalent migrations) before assigning the global table.

Example (Mongo):

 func LocateOrgUnitCustomRoleTable(client db.StoreClient) (*OrgUnitCustomRoleTable, error) {
@@
-	err := tbl.Initialize(col)
+	err := tbl.Initialize(col)
 	if err != nil {
 		return nil, err
 	}
+
+	if mongoCol, ok := col.(*mongo.Collection); ok {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		indexModel := mongo.IndexModel{
+			Keys: bson.D{
+				{Key: "key.tenant", Value: 1},
+				{Key: "key.orgUnitId", Value: 1},
+				{Key: "key.name", Value: 1},
+			},
+			Options: options.Index().SetUnique(true),
+		}
+		if _, err := mongoCol.Indexes().CreateOne(ctx, indexModel); err != nil && !isIndexExistsError(err) {
+			return nil, errors.Wrapf(err, "failed to ensure unique index on org unit custom roles")
+		}
+	}

Reuse whatever helpers we already have for index-exists detection.

Without this, the store permits duplicate custom role documents. Based on learnings

api/swagger/apidocs.swagger.json (2)

2508-2554: Sync Create/Update body validation with the server.

The server rejects blank name, displayName, description, and enforces 1–20 permissions. The OpenAPI still marks everything optional, so generated clients will happily send payloads the backend rejects.

Please:

  • add minLength: 1 to those three strings,
  • set permissions to minItems: 1, maxItems: 20,
  • declare "required": ["name","displayName","description","permissions"] on the create schema and "required": ["displayName","description","permissions"] on the update schema.

That keeps clients and server in agreement. Based on learnings


3608-3644: Add required + size constraints for apiRolePermission.

RolePermission must have a non-empty resource, at least one verb, and each verb must be non-empty. The proto/server enforce this, but the schema leaves everything optional. Update the definition to include:

"required": ["resource", "verbs"],
"properties": {
  "resource": { "type": "string", "minLength": 1, ... },
  "verbs": {
    "type": "array",
    "minItems": 1,
    "items": { "type": "string", "minLength": 1 }
  }
}

Otherwise clients can submit payloads that the backend refuses. Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 44084ee209f7b25f4671aa2dc4e7615037b74e87 and 8838afb.

⛔ Files ignored due to path filters (3)
  • api/org-unit-role.pb.go is excluded by !**/*.pb.go
  • api/org-unit-role.pb.gw.go is excluded by !**/*.pb.gw.go
  • api/org-unit-role_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • api/org-unit-role.pb.route.go (1 hunks)
  • api/org-unit-role.proto (4 hunks)
  • api/swagger/apidocs.swagger.json (8 hunks)
  • main.go (1 hunks)
  • pkg/gateway/routes.go (3 hunks)
  • pkg/gateway/server.go (8 hunks)
  • pkg/server/org-unit-role.go (2 hunks)
  • pkg/server/org-unit-user.go (7 hunks)
  • pkg/table/const.go (1 hunks)
  • pkg/table/org-unit-custom-role.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/table/const.go
  • pkg/gateway/routes.go
  • main.go
🧰 Additional context used
🪛 ast-grep (0.39.6)
pkg/gateway/server.go

[warning] 362-362: MinVersionis missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{InsecureSkipVerify: true}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

🪛 Checkov (3.2.334)
api/swagger/apidocs.swagger.json

[medium] 2522-2529: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

Comment thread pkg/gateway/server.go
Comment on lines +680 to +717
if ouUser.Role == "admin" || ouUser.Role == "auditor" {
return nil, nil
}

// Get the custom role
customRole, err := s.ouCustomRoleTbl.FindByNameAndOrgUnit(r.Context(), authInfo.Realm, ou, ouUser.Role)
if err != nil {
return nil, nil
}

// Collect both Allow and Deny rules for the specified resource type
var allowRules []*table.ResourceMatch
var denyRules []*table.ResourceMatch

for _, perm := range customRole.Permissions {
// Skip non-matching resources early
if perm.Resource != resourceType && perm.Resource != "*" {
continue
}

// Skip permissions without match criteria
if perm.Match == nil {
continue
}

action := perm.Action
if action == "" {
action = "Allow"
}

switch action {
case "Allow":
allowRules = append(allowRules, perm.Match)
case "Deny":
denyRules = append(denyRules, perm.Match)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Filter rules must respect verbs.

getResourceFilterRules appends allow/deny matches without checking perm.Verbs, so a deny on ["delete"] will also hide resources from GET/list, and vice versa. That breaks PBAC semantics and can both over-expose and over-restrict results. Gate these matches on verb (e.g., require * or the current route verb) so we only use rules that actually apply to the request.

Example fix:

-func (s *gateway) getResourceFilterRules(... resourceType string, r *http.Request) ([]*table.ResourceMatch, []*table.ResourceMatch) {
+func (s *gateway) getResourceFilterRules(... resourceType, routeVerb string, r *http.Request) ([]*table.ResourceMatch, []*table.ResourceMatch) {
@@
-		switch action {
-		case "Allow":
-			allowRules = append(allowRules, perm.Match)
-		case "Deny":
-			denyRules = append(denyRules, perm.Match)
+		if verbMatches(routeVerb, perm.Verbs) {
+			switch action {
+			case "Allow":
+				allowRules = append(allowRules, perm.Match)
+			case "Deny":
+				denyRules = append(denyRules, perm.Match)
+			}
 		}

and pass routeInfo.Verb at the call site.

Add a small helper like:

func verbMatches(routeVerb string, verbs []string) bool {
	for _, v := range verbs {
		if v == "*" || strings.EqualFold(v, routeVerb) {
			return true
		}
	}
	return false
}

Without this fix, verb-scoped denies/permits behave unpredictably.

🤖 Prompt for AI Agents
In pkg/gateway/server.go around lines 680 to 717, the loop collecting allow/deny
rules ignores perm.Verbs so verb-scoped permissions (e.g., only "delete") are
incorrectly applied to other verbs; update the call site to pass the current
route verb (e.g., routeInfo.Verb) into getResourceFilterRules, add a small
helper that returns true when the rule verbs include "*" or match the route verb
(case-insensitive), and then gate appending perm.Match to allowRules/denyRules
by calling that helper (skip the permission if it doesn't match the current
verb).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant