Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions internal/servers/permission_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,82 @@ func (r *PermissionServer) Check(ctx context.Context, request *v1.PermissionChec
return response, nil
}

// BulkCheck - Performs multiple authorization checks in a single request
func (r *PermissionServer) BulkCheck(ctx context.Context, request *v1.PermissionBulkCheckRequest) (*v1.PermissionBulkCheckResponse, error) {
ctx, span := internal.Tracer.Start(ctx, "permissions.bulk-check")
defer span.End()

// Validate tenant_id
if request.GetTenantId() == "" {
err := status.Error(GetStatus(nil), "tenant_id is required")
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
}

// Process each check request
results := make([]*v1.PermissionCheckResponse, len(request.GetItems()))
for i, checkRequestItem := range request.GetItems() {

// 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
}
Comment on lines +82 to +93
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.


// Perform the check using existing Check function
checkRequest := &v1.PermissionCheckRequest{
TenantId: request.GetTenantId(),
Subject: checkRequestItem.GetSubject(),
Entity: checkRequestItem.GetEntity(),
Permission: checkRequestItem.GetPermission(),
Metadata: request.GetMetadata(),
Context: request.GetContext(),
Arguments: request.GetArguments(),
}
Comment on lines +96 to +104
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.

response, err := r.invoker.Check(ctx, checkRequest)
if err != nil {
// Log error but don't fail the entire bulk operation
slog.ErrorContext(ctx, "check failed in bulk operation", "error", err.Error(), "index", i)
results[i] = &v1.PermissionCheckResponse{
Can: v1.CheckResult_CHECK_RESULT_DENIED,
Metadata: &v1.PermissionCheckResponseMetadata{
CheckCount: 0,
},
}
continue
}

results[i] = response
}

return &v1.PermissionBulkCheckResponse{
Results: results,
}, nil
}

// Expand - Get schema actions in a tree structure
func (r *PermissionServer) Expand(ctx context.Context, request *v1.PermissionExpandRequest) (*v1.PermissionExpandResponse, error) {
ctx, span := internal.Tracer.Start(ctx, "permissions.expand")
Expand Down
88 changes: 88 additions & 0 deletions proto/base/v1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ service Permission {
};
}

// BulkCheck method receives a PermissionBulkCheckRequest containing multiple check requests
// and returns a PermissionBulkCheckResponse with results for each request.
// Maximum 100 requests can be processed in a single bulk operation.
rpc BulkCheck(PermissionBulkCheckRequest) returns (PermissionBulkCheckResponse) {
// HTTP mapping for this method
option (google.api.http) = {
post: "/v1/tenants/{tenant_id}/permissions/bulk-check"
body: "*"
};
// OpenAPI annotations for this method
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "bulk check api"
tags: ["Permission"]
operation_id: "permissions.bulk-check"
description: "Check multiple permissions in a single request. Maximum 100 requests allowed."
};
}

// Expand method receives a PermissionExpandRequest and returns a PermissionExpandResponse.
// It expands relationships according to the schema provided.
rpc Expand(PermissionExpandRequest) returns (PermissionExpandResponse) {
Expand Down Expand Up @@ -852,6 +870,76 @@ message PermissionCheckResponseMetadata {
int32 check_count = 1 [json_name = "check_count"];
}

// BULK CHECK
message PermissionBulkCheckRequestItem {
// Entity on which the permission needs to be checked, required.
Entity entity = 1 [
json_name = "entity",
(validate.rules).message.required = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"repository:1\""}
];

// Name of the permission or relation, required, must start with a letter and can include alphanumeric and underscore, max 64 bytes.
string permission = 2 [
json_name = "permission",
(validate.rules).string = {
pattern: "^[a-zA-Z_]{1,64}$"
max_bytes: 64
ignore_empty: false
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "The action the user wants to perform on the resource"}
];

// Subject for which the permission needs to be checked, required.
Subject subject = 3 [
json_name = "subject",
(validate.rules).message.required = true
];
}
// PermissionBulkCheckRequest is the request message for the BulkCheck method in the Permission service.
message PermissionBulkCheckRequest {
// Identifier of the tenant, required, and must match the pattern "[a-zA-Z0-9-,]+", max 64 bytes.
string tenant_id = 1 [
json_name = "tenant_id",
(validate.rules).string = {
pattern: "^([a-zA-Z0-9_\\-@\\.:+]{1,128}|\\*)$"
max_bytes: 128
ignore_empty: false
},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Identifier of the tenant, if you are not using multi-tenancy (have only one tenant) use pre-inserted tenant <code>t1</code> for this field. Required, and must match the pattern \\“[a-zA-Z0-9-,]+\\“, max 64 bytes."}
];

// Metadata associated with this request, required.
PermissionCheckRequestMetadata metadata = 2 [
json_name = "metadata",
(validate.rules).message.required = true
];

// List of permission check requests, maximum 100 items.
repeated PermissionBulkCheckRequestItem items = 3 [
json_name = "items",
(validate.rules).repeated = {
min_items: 1
max_items: 100
}
];

// Context associated with this request.
Context context = 4 [
json_name = "context",
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Contextual data that can be dynamically added to permission check requests. See details on [Contextual Data](../../operations/contextual-tuples)"}
];

// Additional arguments associated with this request.
repeated Argument arguments = 5 [json_name = "arguments"];
}

// PermissionBulkCheckResponse is the response message for the BulkCheck method in the Permission service.
message PermissionBulkCheckResponse {
// List of permission check responses corresponding to each request.
repeated PermissionCheckResponse results = 1 [json_name = "results"];
}

// EXPAND

// PermissionExpandRequest is the request message for the Expand method in the Permission service.
Expand Down