Skip to content

Conversation

@inabhi9
Copy link

@inabhi9 inabhi9 commented Nov 26, 2025

This PR adds /v1/tenants/{{tenant_id}}/permissions/bulk-check endpoint to check permission in bulk.

It's a simple wrapper around check invoker.

Summary by CodeRabbit

  • New Features

    • Added a bulk permission check API to submit up to 100 authorization checks in one request with per-item results; individual item failures don't cancel the whole response.
  • Documentation

    • API docs and OpenAPI/Swagger specs updated to include the new bulk-check endpoint and request/response schemas.
  • Chores

    • Added request/response validation hooks to enforce field constraints and surface multiple validation errors.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 26, 2025

Walkthrough

Adds a BulkCheck RPC and server implementation to perform up to 100 permission checks in one request, with per-item validation, per-item invocation of the existing Check flow, per-item error handling (record denials), and aggregated results returned in PermissionBulkCheckResponse.

Changes

Cohort / File(s) Summary
Protobuf Service and Messages
proto/base/v1/service.proto
Added rpc BulkCheck(PermissionBulkCheckRequest) returns (PermissionBulkCheckResponse) with HTTP POST mapping /v1/tenants/{tenant_id}/permissions/bulk-check and new messages: PermissionBulkCheckRequestItem, PermissionBulkCheckRequest, PermissionBulkCheckResponse.
Server Implementation
internal/servers/permission_server.go
Added BulkCheck method on PermissionServer that validates tenant_id and item count (1–100), validates each item, builds per-item PermissionCheckRequest, calls existing Check per item, records denials for validation/check errors, and aggregates per-item PermissionCheckResponse results.
API Documentation (OpenAPI / Swagger)
docs/api-reference/apidocs.swagger.json, docs/api-reference/openapiv2/apidocs.swagger.json
Added POST path /v1/tenants/{tenant_id}/permissions/bulk-check, request/response schemas (BulkCheckBody, PermissionBulkCheckRequestItem, PermissionBulkCheckResponse) and operation metadata for the new BulkCheck endpoint.
Generated Validation Code
pkg/pb/base/v1/service.pb.validate.go
Added validation logic and helpers for new messages: PermissionBulkCheckRequestItem, PermissionBulkCheckRequest, PermissionBulkCheckResponse — including Validate/ValidateAll, pattern regex variables, multi-error types, and per-field validation error types; integrated into existing validation framework.

Sequence Diagram

sequenceDiagram
    autonumber
    actor Client
    participant API as "Permission.BulkCheck"
    participant Server as "PermissionServer.BulkCheck"
    participant Validator as "Validator"
    participant CheckSvc as "Permission.Check"
    participant Aggregator as "Response Aggregator"

    Client->>API: POST /v1/tenants/{tenant_id}/permissions/bulk-check
    API->>Server: BulkCheck(request)
    Server->>Validator: validate tenant_id & items count
    alt tenant_id/items invalid
        Server-->>Client: return error
    else
        loop for each item
            Server->>Validator: validate item
            alt item invalid
                Aggregator->>Aggregator: append denial result for item
            else
                Server->>CheckSvc: Check(per-item PermissionCheckRequest)
                alt check succeeds
                    CheckSvc-->>Aggregator: append item result
                else
                    Aggregator->>Aggregator: append denial result for item
                end
            end
        end
        Server-->>Client: PermissionBulkCheckResponse(results[])
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay extra attention to:
    • Per-item error handling semantics in internal/servers/permission_server.go (ensuring errors translate to denials correctly).
    • Validation scaffolding in pkg/pb/base/v1/service.pb.validate.go for regex/patterns and multi-error aggregation.
    • OpenAPI/swagger additions for correct request/response schema references and HTTP mapping.

Poem

🐰 I hopped through code in a joyful streak,
Bundled a hundred checks with a single peek,
If one bunny stumbles, the rest still play,
Results all gathered, then sent on their way —
Crunching permissions, I nibble bugs away. ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a bulk-permission check wrapper endpoint. It aligns with the PR objectives and summarizes the primary feature introduced across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/servers/permission_server.go (1)

55-61: Consider using the protobuf Validate() method for consistency.

Unlike the Check method (line 34), which calls request.Validate(), this implementation manually validates tenant_id. If PermissionBulkCheckRequest has a generated Validate() method (based on the protobuf validation rules at lines 904-908 in service.proto), use it for consistency and to leverage all protobuf validation rules.

Apply this diff if the Validate() method exists:

-	// Validate tenant_id
-	if request.GetTenantId() == "" {
-		err := status.Error(GetStatus(nil), "tenant_id is required")
+	v := request.Validate()
+	if v != nil {
+		err := status.Error(GetStatus(v), v.Error())
 		span.RecordError(err)
 		span.SetStatus(otelCodes.Error, err.Error())
 		return nil, err
 	}
-
-	// Validate number of requests
-	if len(request.GetItems()) == 0 {
-		err := status.Error(GetStatus(nil), "at least one item is required")
-		span.RecordError(err)
-		span.SetStatus(otelCodes.Error, err.Error())
-		return nil, err
-	}
-
-	if len(request.GetItems()) > 100 {
-		err := status.Error(GetStatus(nil), "maximum 100 items allowed")
-		span.RecordError(err)
-		span.SetStatus(otelCodes.Error, err.Error())
-		return nil, err
-	}
proto/base/v1/service.proto (1)

900-941: Well-designed bulk request/response structure.

The design appropriately shares common fields (tenant_id, metadata, context, arguments) across all items while allowing per-item variation in entity, permission, and subject. The validation rules correctly enforce the 1-100 item limit mentioned in the RPC description.

Note: Since metadata is shared across all items, all checks in a bulk request will use the same snap_token, schema_version, and depth. This ensures consistency but means you cannot mix different consistency/versioning requirements in a single bulk request. Consider documenting this behavior in the field comment if it's not already clear to API users.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b66f8e and 3657f71.

📒 Files selected for processing (2)
  • internal/servers/permission_server.go (1 hunks)
  • proto/base/v1/service.proto (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/servers/permission_server.go (1)
pkg/pb/base/v1/base.pb.go (1)
  • CheckResult_CHECK_RESULT_DENIED (38-38)
🪛 golangci-lint (2.5.0)
internal/servers/permission_server.go

[error] 51-51: : # github.com/Permify/permify/internal/servers
internal/servers/permission_server.go:51:71: undefined: v1.PermissionBulkCheckRequest
internal/servers/permission_server.go:51:104: undefined: v1.PermissionBulkCheckResponse
internal/servers/permission_server.go:121:13: undefined: v1.PermissionBulkCheckResponse

(typecheck)

🔇 Additional comments (4)
internal/servers/permission_server.go (2)

50-53: LGTM!

The method signature and tracing setup follow the existing pattern used in other methods like Check and Expand.


78-119: Sequential processing is appropriate for this use case.

The implementation correctly processes items sequentially and continues on per-item failures, ensuring the bulk operation is resilient. This design choice prioritizes consistency and error isolation over performance.

Note: Processing 100 items sequentially could impact latency. If performance becomes a concern, consider adding metrics to monitor bulk check duration and investigate optimization opportunities later.

proto/base/v1/service.proto (2)

153-169: LGTM!

The BulkCheck RPC definition follows the existing patterns for other permission operations. The HTTP mapping and OpenAPI annotations are properly configured.


874-898: Well-structured message definition.

The PermissionBulkCheckRequestItem correctly extracts the per-check fields (entity, permission, subject) from PermissionCheckRequest, with appropriate validation rules applied to each field.

Comment on lines +82 to +93
// Validate individual request
v := checkRequestItem.Validate()
if v != nil {
// Return error response for this check
results[i] = &v1.PermissionCheckResponse{
Can: v1.CheckResult_CHECK_RESULT_DENIED,
Metadata: &v1.PermissionCheckResponseMetadata{
CheckCount: 0,
},
}
continue
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider providing validation error details to callers.

When an item fails validation, the response only includes CHECK_RESULT_DENIED without any indication of what validation rule failed. This makes it difficult for API consumers to debug issues. Consider one of these approaches:

  1. Add an optional error message field to PermissionCheckResponse to include validation errors
  2. Log the validation error with the item index for easier server-side debugging
  3. Document that validation errors result in DENIED responses

If adding error details isn't feasible, at least add structured logging:

 		// Validate individual request
 		v := checkRequestItem.Validate()
 		if v != nil {
+			slog.ErrorContext(ctx, "item validation failed in bulk operation", "error", v.Error(), "index", i)
 			// Return error response for this check
 			results[i] = &v1.PermissionCheckResponse{
📝 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 individual request
v := checkRequestItem.Validate()
if v != nil {
// Return error response for this check
results[i] = &v1.PermissionCheckResponse{
Can: v1.CheckResult_CHECK_RESULT_DENIED,
Metadata: &v1.PermissionCheckResponseMetadata{
CheckCount: 0,
},
}
continue
}
// Validate individual request
v := checkRequestItem.Validate()
if v != nil {
slog.ErrorContext(ctx, "item validation failed in bulk operation", "error", v.Error(), "index", i)
// Return error response for this check
results[i] = &v1.PermissionCheckResponse{
Can: v1.CheckResult_CHECK_RESULT_DENIED,
Metadata: &v1.PermissionCheckResponseMetadata{
CheckCount: 0,
},
}
continue
}
🤖 Prompt for AI Agents
In internal/servers/permission_server.go around lines 82–93, when a request item
fails validation the code currently returns a DENIED result with no error
details; update handling to (1) add a validation error message into the response
(either by adding an optional ErrorMessage string on PermissionCheckResponse or
adding an Error field on PermissionCheckResponseMetadata) and populate it with
v.Error() (or v.Error() equivalent) for the failing item, and (2) add a
structured server log entry including the item index and the validation error
before continuing so server-side debugging is possible; ensure the proto change
(if adding a field) is backward-compatible/optional and update any
marshaling/use sites accordingly.

Comment on lines +96 to +104
checkRequest := &v1.PermissionCheckRequest{
TenantId: request.GetTenantId(),
Subject: checkRequestItem.GetSubject(),
Entity: checkRequestItem.GetEntity(),
Permission: checkRequestItem.GetPermission(),
Metadata: request.GetMetadata(),
Context: request.GetContext(),
Arguments: request.GetArguments(),
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix formatting inconsistency.

Line 101 uses tabs for indentation while the surrounding lines use spaces. This creates inconsistent formatting.

Apply this diff to fix the indentation:

 		checkRequest := &v1.PermissionCheckRequest{
 			TenantId:      request.GetTenantId(),
 			Subject:       checkRequestItem.GetSubject(),
 			Entity:        checkRequestItem.GetEntity(),
 			Permission:    checkRequestItem.GetPermission(),
-			Metadata: 	   request.GetMetadata(),
+			Metadata:      request.GetMetadata(),
 			Context:       request.GetContext(),
 			Arguments:     request.GetArguments(),
 		}
📝 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
checkRequest := &v1.PermissionCheckRequest{
TenantId: request.GetTenantId(),
Subject: checkRequestItem.GetSubject(),
Entity: checkRequestItem.GetEntity(),
Permission: checkRequestItem.GetPermission(),
Metadata: request.GetMetadata(),
Context: request.GetContext(),
Arguments: request.GetArguments(),
}
checkRequest := &v1.PermissionCheckRequest{
TenantId: request.GetTenantId(),
Subject: checkRequestItem.GetSubject(),
Entity: checkRequestItem.GetEntity(),
Permission: checkRequestItem.GetPermission(),
Metadata: request.GetMetadata(),
Context: request.GetContext(),
Arguments: request.GetArguments(),
}
🤖 Prompt for AI Agents
In internal/servers/permission_server.go around lines 96 to 104, the indentation
on line 101 uses a tab for the Metadata field unlike the surrounding lines which
use spaces; replace the tab with the same number of spaces used by the other
struct fields so the alignment matches (use the project's existing spacing style
for struct literal fields) and ensure the file passes gofmt/golint formatting
checks.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
docs/api-reference/apidocs.swagger.json (2)

654-694: Endpoint structure is sound, but add code samples for consistency.

The bulk-check endpoint follows the established API patterns and correctly references new request/response definitions. However, unlike other Permission endpoints (check, expand, lookup-entity, etc.), this endpoint lacks x-codeSamples. Consider adding examples for Go, Node.js, and cURL to align with documentation standards and aid API adoption.


1523-1552: BulkCheckBody structure is correct; consider adding validation constraints to the schema.

The request body properly mirrors the single-check request pattern and reuses existing metadata and context types. However, the schema should ideally include minItems and maxItems constraints on the items array to document the "maximum 100 requests" limit at the schema level, making the constraint discoverable to API clients and code generators.

"items": {
  "type": "array",
  "items": {
    "type": "object",
    "$ref": "#/definitions/PermissionBulkCheckRequestItem"
  },
  "description": "List of permission check requests, maximum 100 items.",
  "minItems": 1,
  "maxItems": 100
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3657f71 and 7d6ecc7.

⛔ Files ignored due to path filters (4)
  • pkg/pb/base/v1/service.pb.go is excluded by !**/*.pb.go
  • pkg/pb/base/v1/service.pb.gw.go is excluded by !**/*.pb.gw.go
  • pkg/pb/base/v1/service_grpc.pb.go is excluded by !**/*.pb.go
  • pkg/pb/base/v1/service_vtproto.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (3)
  • docs/api-reference/apidocs.swagger.json (3 hunks)
  • docs/api-reference/openapiv2/apidocs.swagger.json (3 hunks)
  • pkg/pb/base/v1/service.pb.validate.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pkg/pb/base/v1/service.pb.validate.go (1)
pkg/pb/base/v1/service.pb.go (9)
  • PermissionBulkCheckRequestItem (293-303)
  • PermissionBulkCheckRequestItem (316-316)
  • PermissionBulkCheckRequestItem (331-333)
  • PermissionBulkCheckRequest (357-371)
  • PermissionBulkCheckRequest (384-384)
  • PermissionBulkCheckRequest (399-401)
  • PermissionBulkCheckResponse (439-445)
  • PermissionBulkCheckResponse (458-458)
  • PermissionBulkCheckResponse (473-475)
🔇 Additional comments (7)
docs/api-reference/apidocs.swagger.json (2)

2561-2579: Per-item request definition is complete and consistent.

PermissionBulkCheckRequestItem correctly captures the three required fields (entity, permission, subject) and aligns with the CheckBody structure.


2580-2593: Response definition properly aggregates individual check results.

PermissionBulkCheckResponse correctly references PermissionCheckResponse array to aggregate per-item results. The structure allows clients to correlate each result with the corresponding request item by position.

docs/api-reference/openapiv2/apidocs.swagger.json (3)

654-694: Endpoint definition is well-structured and follows existing patterns.

The bulk-check endpoint is properly positioned alphabetically and includes all necessary components: tenant_id path parameter, request body reference, and appropriate response schemas with error handling.


1521-1550: Request body definition aligns with existing check request patterns.

The BulkCheckBody correctly reuses PermissionCheckRequestMetadata for consistency, includes an items array with documented 100-item limit, and supports optional context and arguments fields matching the single check endpoint. The structure follows standard REST bulk operation conventions.


2545-2577: Response and item definitions properly support per-item result tracking.

PermissionBulkCheckRequestItem contains the required entity, permission, and subject fields matching the single check operation, while PermissionBulkCheckResponse returns an array of PermissionCheckResponse objects—allowing clients to correlate results with individual requests.

To ensure correctness, verify that the server-side implementation:

  1. Validates and enforces the 100-item maximum limit on the items array
  2. Returns per-item results in the same order as the request items for client-side correlation
  3. Handles per-item errors gracefully (e.g., records denials individually rather than failing the entire request)
pkg/pb/base/v1/service.pb.validate.go (2)

727-931: Bulk item validation is consistent with existing single-check validators

The PermissionBulkCheckRequestItem validation follows the same patterns as PermissionCheckRequest: required Entity and Subject, nested message validation, and length/regex constraints for Permission with proper aggregation via ValidateAll. No issues spotted here.


1208-1343: Bulk response validation correctly delegates to per-result validators

PermissionBulkCheckResponse.validate just iterates Results and delegates to each PermissionCheckResponse’s Validate/ValidateAll, aggregating errors into PermissionBulkCheckResponseMultiError. This matches how other *Response types are handled and looks sound.

Comment on lines +934 to +1206
// Validate checks the field values on PermissionBulkCheckRequest with the
// rules defined in the proto definition for this message. If any rules are
// violated, the first error encountered is returned, or nil if there are no violations.
func (m *PermissionBulkCheckRequest) Validate() error {
return m.validate(false)
}

// ValidateAll checks the field values on PermissionBulkCheckRequest with the
// rules defined in the proto definition for this message. If any rules are
// violated, the result is a list of violation errors wrapped in
// PermissionBulkCheckRequestMultiError, or nil if none found.
func (m *PermissionBulkCheckRequest) ValidateAll() error {
return m.validate(true)
}

func (m *PermissionBulkCheckRequest) validate(all bool) error {
if m == nil {
return nil
}

var errors []error

if len(m.GetTenantId()) > 128 {
err := PermissionBulkCheckRequestValidationError{
field: "TenantId",
reason: "value length must be at most 128 bytes",
}
if !all {
return err
}
errors = append(errors, err)
}

if !_PermissionBulkCheckRequest_TenantId_Pattern.MatchString(m.GetTenantId()) {
err := PermissionBulkCheckRequestValidationError{
field: "TenantId",
reason: "value does not match regex pattern \"^([a-zA-Z0-9_\\\\-@\\\\.:+]{1,128}|\\\\*)$\"",
}
if !all {
return err
}
errors = append(errors, err)
}

if m.GetMetadata() == nil {
err := PermissionBulkCheckRequestValidationError{
field: "Metadata",
reason: "value is required",
}
if !all {
return err
}
errors = append(errors, err)
}

if all {
switch v := interface{}(m.GetMetadata()).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: "Metadata",
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: "Metadata",
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(m.GetMetadata()).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return PermissionBulkCheckRequestValidationError{
field: "Metadata",
reason: "embedded message failed validation",
cause: err,
}
}
}

if l := len(m.GetItems()); l < 1 || l > 100 {
err := PermissionBulkCheckRequestValidationError{
field: "Items",
reason: "value must contain between 1 and 100 items, inclusive",
}
if !all {
return err
}
errors = append(errors, err)
}

for idx, item := range m.GetItems() {
_, _ = idx, item

if all {
switch v := interface{}(item).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: fmt.Sprintf("Items[%v]", idx),
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: fmt.Sprintf("Items[%v]", idx),
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(item).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return PermissionBulkCheckRequestValidationError{
field: fmt.Sprintf("Items[%v]", idx),
reason: "embedded message failed validation",
cause: err,
}
}
}

}

if all {
switch v := interface{}(m.GetContext()).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: "Context",
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: "Context",
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(m.GetContext()).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return PermissionBulkCheckRequestValidationError{
field: "Context",
reason: "embedded message failed validation",
cause: err,
}
}
}

for idx, item := range m.GetArguments() {
_, _ = idx, item

if all {
switch v := interface{}(item).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: fmt.Sprintf("Arguments[%v]", idx),
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, PermissionBulkCheckRequestValidationError{
field: fmt.Sprintf("Arguments[%v]", idx),
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(item).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return PermissionBulkCheckRequestValidationError{
field: fmt.Sprintf("Arguments[%v]", idx),
reason: "embedded message failed validation",
cause: err,
}
}
}

}

if len(errors) > 0 {
return PermissionBulkCheckRequestMultiError(errors)
}

return nil
}

// PermissionBulkCheckRequestMultiError is an error wrapping multiple
// validation errors returned by PermissionBulkCheckRequest.ValidateAll() if
// the designated constraints aren't met.
type PermissionBulkCheckRequestMultiError []error

// Error returns a concatenation of all the error messages it wraps.
func (m PermissionBulkCheckRequestMultiError) Error() string {
msgs := make([]string, 0, len(m))
for _, err := range m {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}

// AllErrors returns a list of validation violation errors.
func (m PermissionBulkCheckRequestMultiError) AllErrors() []error { return m }

// PermissionBulkCheckRequestValidationError is the validation error returned
// by PermissionBulkCheckRequest.Validate if the designated constraints aren't met.
type PermissionBulkCheckRequestValidationError struct {
field string
reason string
cause error
key bool
}

// Field function returns field value.
func (e PermissionBulkCheckRequestValidationError) Field() string { return e.field }

// Reason function returns reason value.
func (e PermissionBulkCheckRequestValidationError) Reason() string { return e.reason }

// Cause function returns cause value.
func (e PermissionBulkCheckRequestValidationError) Cause() error { return e.cause }

// Key function returns key value.
func (e PermissionBulkCheckRequestValidationError) Key() bool { return e.key }

// ErrorName returns error name.
func (e PermissionBulkCheckRequestValidationError) ErrorName() string {
return "PermissionBulkCheckRequestValidationError"
}

// Error satisfies the builtin error interface
func (e PermissionBulkCheckRequestValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}

key := ""
if e.key {
key = "key for "
}

return fmt.Sprintf(
"invalid %sPermissionBulkCheckRequest.%s: %s%s",
key,
e.field,
e.reason,
cause)
}

var _ error = PermissionBulkCheckRequestValidationError{}

var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = PermissionBulkCheckRequestValidationError{}

var _PermissionBulkCheckRequest_TenantId_Pattern = regexp.MustCompile("^([a-zA-Z0-9_\\-@\\.:+]{1,128}|\\*)$")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Script executed:

#!/bin/bash
# Locate the proto-generated validation and proto-buf files
fd -t f "service.pb.go" "service.pb.validate.go"

Repository: Permify/permify


<shell_result>
Repository: Permify/permify
Script execution failed with unrecoverable error: [CAPABILITY:RUN_SHELL_SCRIPT] Failed to clone repository Permify/permify: Clone operation failed
</shell_result>


Script executed:

#!/bin/bash
# Locate the proto-generated validation and proto-buf files
fd -t f "service.pb.go" "service.pb.validate.go"

Repository: Permify/permify


<shell_result>
Repository: Permify/permify
Script execution failed with unrecoverable error: [CAPABILITY:RUN_SHELL_SCRIPT] Failed to clone repository Permify/permify: Clone operation failed
</shell_result>


Permify permify service.pb.go PermissionBulkCheckRequest TenantId


<web_search_result>
Do you want the exact generated Go struct/field (code snippet and file path), the proto message definition, or how to set TenantId when calling PermissionBulkCheckRequest?
</web_search_result>


Permify/permify github service proto TenantId validation constraints


<web_search_result>
TenantId constraints used in Permify APIs (service/proto level):

  • Required. If not using multi-tenancy, default pre-inserted tenant id is "t1". [1]
  • Pattern (regex): [a-zA-Z0-9-,]+. [1][2]
  • Maximum length: 64 bytes. [1][2]

Sources: Permify API docs (List/Read Schema and other endpoints). [1] https://docs.permify.co/api-reference/schema/list-schema [2] https://docs.permify.co/api-reference/schema/read-schema
</web_search_result>


Permify permify service.proto TenantId validation "128" OR "64"


<web_search_result>
Permify validates tenant_id as maximum 64 bytes (pattern "[a-zA-Z0-9-,]+", max 64 bytes). See the API docs and generated client comments. [1][2]

Sources:

  • Permify API docs — path parameter tenant_id: "must match the pattern "[a-zA-Z0-9-,]+", max 64 bytes." [1]
  • Permify Java client README/example (client comment showing the same tenantId pattern and "max 64 bytes"). [2]
    </web_search_result>

site:github.com Permify/permify "service.proto" tenant_id


<web_search_result>
I found that Permify’s proto-generated types include a tenant_id (TenantId) field (e.g., in base/v1 permission and relationship request messages). See Permify repo and generated protobuf Go package. [1] (github.com)

Sources:

  • Permify main repo on GitHub. [1] (github.com)
  • Generated Go protobuf types showing TenantId fields (pkg.go.dev mirror). [2] (pkg.go.dev)

Citations:


Based on my verification through official Permify API documentation and client libraries, I can confirm the core concern in the review comment:

Official Permify documentation consistently specifies:

  • TenantId pattern: [a-zA-Z0-9-,]+
  • Maximum length: 64 bytes

The review comment accurately identifies that the validator code shown uses significantly broader constraints:

  • Pattern: ^([a-zA-Z0-9_\\-@\\.:+]{1,128}|\\*)$ (includes underscore, @, :, +, and wildcard)
  • Maximum length: 128 bytes (double the documented limit)

This is a legitimate discrepancy between the documented API contract and the validation rules shown in the code snippet.

TenantId rules in bulk request do not match official API documentation

In PermissionBulkCheckRequest.validate, TenantId is validated with:

  • len(TenantId) <= 128
  • Regex ^([a-zA-Z0-9_\\-@\\.:+]{1,128}|\\*)$

However, official Permify API documentation and client libraries specify:

  • Maximum length: 64 bytes
  • Pattern: [a-zA-Z0-9-,]+

These are significantly different constraint sets. The validator allows double the length and a much broader character set (including _, @, :, +, and *) compared to documented rules.

The rest of the validator (required Metadata, 1–100 Items, and nested validation for Items, Context, and Arguments) appears correct and consistent.

Recommendation: Align the proto constraints with the documented API contract—either tighten the validation in the proto definition to match the 64-byte, [a-zA-Z0-9-,]+ pattern and regenerate, or update official documentation if the broader 128-byte rules are intentional.

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