PBAC Frontend Integration Guide¶
This document describes how to use the AccessResourceService that exposes PBAC functionality to frontend apps.
Overview¶
AccessResourceService is a gRPC service provided by PolicyVault for checking user access permissions. It is the primary entry point for any service that needs to answer the question: "Can this user perform this action on this resource/project/company?"
All access checks should go through this service rather than implementing custom authorization logic.
- Proto file:
src/shared/Protos/policy_vault.proto - Package:
policyvault
Methods¶
1. IsAllowed¶
Checks whether a user can perform a specific action on a specific resource within a project.
Use this method when your service needs to verify access to a concrete resource (file, folder, template) before returning it to the user or performing a mutation.
Request: AccessResourceRequest
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string |
Yes | User UUID (e.g., "550e8400-e29b-41d4-a716-446655440000") |
access_level |
string |
Yes | Required access level |
project_id |
string |
Yes | Project ID in which the resource resides |
resource_id |
ResourceIdValue |
Yes | Identifier of the target resource |
Response: AccessResourceResponse
The response uses a oneof result field. Exactly one of the following fields will be set:
| Field | Type | Meaning |
|---|---|---|
success |
bool |
true — access is granted |
user_not_found_id |
string |
The user with this ID was not found in PolicyVault |
resource_not_found |
ResourceNotFoundDetail |
The resource was not found (contains resource_id and message) |
access_denied_message |
string |
Access denied by policy (contains the reason) |
exception_message |
string |
An unexpected server error occurred |
2. IsAllowedProject¶
Checks whether a user can perform a specific action on a project (without specifying a concrete resource).
Use this method when you need to verify general project-level access — for example, before listing project contents, or before any operation scoped to the project.
Request: AccessProjectRequest
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string |
Yes | User UUID |
access_level |
string |
Yes | Required access level |
project_id |
string |
Yes | Target project ID |
Response: AccessResourceResponse
Same response format as IsAllowed.
3. IsProjectUserRole¶
Returns the user's role in a specific project.
Use this method when you need to know the user's role to adjust UI or behavior (e.g., showing/hiding edit controls) without making an Allow/Deny decision.
Request: RoleProjectRequest
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string |
Yes | User UUID |
project_id |
string |
Yes | Target project ID |
Response: ProjectRoleResponse
Uses a oneof result field:
| Field | Type | Meaning |
|---|---|---|
role |
ProjectRole |
User's role was found. Contains project_id, role, project_name |
error |
string |
Error message (project not found, user not a member) |
Possible role values: "owner", "admin", "contributor", "viewer", "custom:<label>"
4. IsCompanyUserRole¶
Returns the user's scope in a specific company.
Use this method when you need to know the user's company-level permissions.
Request: CompanyProjectRequest
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string |
Yes | User UUID |
company_id |
string |
Yes | Target company ID |
Response: CompanyRoleResponse
Uses a oneof result field:
| Field | Type | Meaning |
|---|---|---|
role |
CompanyRole |
User's scope was found. Contains company_id, role, company_name |
error |
string |
Error message (company not found, user not a member) |
Possible role values: "owner", "admin", "editor", "viewer", "member"
5. GetAvailableProject¶
Returns all projects accessible to a user, along with the user's role in each.
Use this method to populate project selectors, dashboards, or any list of projects visible to the current user.
Request: UserRequest
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string |
Yes | User UUID |
Response: AvailableProjectResponse
| Field | Type | Description |
|---|---|---|
projects |
repeated ProjectRole |
List of projects with the user's role in each |
Each ProjectRole contains:
| Field | Type | Description |
|---|---|---|
project_id |
string |
Project ID |
role |
string |
User's role: "owner", "admin", "contributor", "viewer", "custom:<label>", or "member" |
project_name |
string |
Human-readable project name |
Errors: throws gRPC NOT_FOUND if the user does not exist.
6. GetAvailableCompanies¶
Returns all companies accessible to a user, along with the user's scope in each.
Use this method to populate company selectors or tenant-scoped navigation.
Request: UserRequest
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string |
Yes | User UUID |
Response: AvailableCompanyResponse
| Field | Type | Description |
|---|---|---|
companies |
repeated CompanyRole |
List of companies with the user's scope in each |
Each CompanyRole contains:
| Field | Type | Description |
|---|---|---|
company_id |
string |
Company ID |
role |
string |
User's scope: "owner", "admin", "editor", "viewer", or "member" |
company_name |
string |
Human-readable company name |
Errors: throws gRPC NOT_FOUND if the user does not exist.
Reference types¶
Access levels¶
The access_level field is a string. Supported values:
| Value | Meaning | Minimum company scope | Minimum project role |
|---|---|---|---|
"read" |
Read access | Viewer | Viewer |
"write" |
Write access | Editor | Contributor |
"admin" |
Administrative access | Admin | Admin |
"custom:<label>" |
Custom access level | — | — |
Resource IDs¶
ResourceIdValue is a oneof message representing the target resource:
message ResourceIdValue {
oneof kind {
string common = 1; // generic string identifier
BinaryAssetIdValue file_asset = 2; // typed file asset
}
}
message BinaryAssetIdValue {
oneof kind {
string single_file = 1; // path to a single file
string folder = 2; // path to a folder
string template = 3; // template identifier
}
}
Access evaluation logic¶
When IsAllowed or IsAllowedProject is called, PolicyVault evaluates access hierarchically:
1. Company-level check (for company projects):
- Owner of company? -> Allow
- Not a member of company? -> Deny
- User's scope >= required level? -> Allow
- Otherwise -> Deny
2. Project-level check:
- Owner of project? -> Allow
- Not a member of project? -> Deny
- User's role >= required level? -> Allow
- Otherwise -> Deny
3. Resource scope check (IsAllowed only):
- Owner of project? -> access to all resources
- Resource shared as "Anyone"? -> Allow
- Resource shared as "Personal" and user is listed? -> Allow
- Otherwise -> Deny
A personal project (not attached to a company) skips the company-level check.
Integration guide: Node.js¶
Step 1: Generate client code¶
Copy the proto file into your project and generate the client stubs.
Using grpc-tools + grpc_tools_node_protoc_ts:
npm install @grpc/grpc-js @grpc/proto-loader
# or, for static codegen:
npm install grpc-tools grpc_tools_node_protoc_ts --save-dev
With @grpc/proto-loader (dynamic loading, no codegen required):
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const packageDef = protoLoader.loadSync("path/to/policy_vault.proto", {
keepCase: false, // converts snake_case to camelCase
longs: String,
enums: String,
defaults: true,
oneofs: true, // required to read oneof field names
});
const proto = grpc.loadPackageDefinition(packageDef);
const policyvault = proto.policyvault;
Step 2: Create the client¶
const POLICY_VAULT_URL = process.env.POLICY_VAULT_URL || "localhost:5001";
// Without TLS (development)
const client = new policyvault.AccessResourceService(
POLICY_VAULT_URL,
grpc.credentials.createInsecure()
);
// With TLS + JWT token
const metadata = new grpc.Metadata();
metadata.add("authorization", `Bearer ${token}`);
const client = new policyvault.AccessResourceService(
POLICY_VAULT_URL,
grpc.credentials.createSsl()
);
Step 3: Call the service¶
Check resource access:
const response = await new Promise((resolve, reject) => {
client.isAllowed(
{
userId: "550e8400-e29b-41d4-a716-446655440000",
accessLevel: "read",
projectId: "my-project",
resourceId: {
fileAsset: { singleFile: "models/v2/weights.bin" },
},
},
metadata,
(err, res) => (err ? reject(err) : resolve(res))
);
});
// With oneofs: true, the `result` field contains the name of the set field
if (response.result === "success") {
// access granted
} else if (response.result === "accessDeniedMessage") {
throw new Error(`Access denied: ${response.accessDeniedMessage}`);
} else if (response.result === "userNotFoundId") {
throw new Error(`User not found: ${response.userNotFoundId}`);
}
Check project-level access:
const response = await new Promise((resolve, reject) => {
client.isAllowedProject(
{
userId: userId,
accessLevel: "write",
projectId: projectId,
},
metadata,
(err, res) => (err ? reject(err) : resolve(res))
);
});
if (response.result !== "success") {
throw new Error("Access denied");
}
Get user's role in a project:
const response = await new Promise((resolve, reject) => {
client.isProjectUserRole(
{ userId: userId, projectId: projectId },
metadata,
(err, res) => (err ? reject(err) : resolve(res))
);
});
if (response.result === "role") {
console.log(`Role: ${response.role.role}`); // "owner", "admin", ...
console.log(`Project: ${response.role.projectName}`);
} else {
console.error(`Error: ${response.error}`);
}
List available projects:
const response = await new Promise((resolve, reject) => {
client.getAvailableProject(
{ userId: userId },
metadata,
(err, res) => (err ? reject(err) : resolve(res))
);
});
for (const project of response.projects) {
console.log(`${project.projectName} (${project.role})`);
}
Helper: promisify the client¶
To avoid callback nesting, wrap the client methods:
const { promisify } = require("util");
const accessClient = {
isAllowed: promisify(client.isAllowed).bind(client),
isAllowedProject: promisify(client.isAllowedProject).bind(client),
isProjectUserRole: promisify(client.isProjectUserRole).bind(client),
isCompanyUserRole: promisify(client.isCompanyUserRole).bind(client),
getAvailableProject: promisify(client.getAvailableProject).bind(client),
getAvailableCompanies: promisify(client.getAvailableCompanies).bind(client),
};
// Usage:
const res = await accessClient.isAllowed(request, metadata);
Integration guide: Python¶
Step 1: Generate client code¶
Install dependencies and generate stubs from the proto file:
pip install grpcio grpcio-tools
python -m grpc_tools.protoc \
-I./protos \
--python_out=./generated \
--grpc_python_out=./generated \
./protos/policy_vault.proto
This produces policy_vault_pb2.py (messages) and policy_vault_pb2_grpc.py (service stubs).
Step 2: Create the client¶
import grpc
import policy_vault_pb2 as pb
import policy_vault_pb2_grpc as pb_grpc
POLICY_VAULT_URL = "localhost:5001"
# Without TLS (development)
channel = grpc.insecure_channel(POLICY_VAULT_URL)
# With TLS + JWT token
credentials = grpc.ssl_channel_credentials()
token_credentials = grpc.access_token_call_credentials(token)
composite = grpc.composite_channel_credentials(credentials, token_credentials)
channel = grpc.secure_channel(POLICY_VAULT_URL, composite)
client = pb_grpc.AccessResourceServiceStub(channel)
Step 3: Call the service¶
Check resource access:
response = client.IsAllowed(
pb.AccessResourceRequest(
user_id="550e8400-e29b-41d4-a716-446655440000",
access_level="read",
project_id="my-project",
resource_id=pb.ResourceIdValue(
file_asset=pb.BinaryAssetIdValue(
single_file="models/v2/weights.bin"
)
),
)
)
result_field = response.WhichOneof("result")
if result_field == "success":
pass # access granted
elif result_field == "access_denied_message":
raise PermissionError(f"Access denied: {response.access_denied_message}")
elif result_field == "user_not_found_id":
raise LookupError(f"User not found: {response.user_not_found_id}")
elif result_field == "resource_not_found":
raise LookupError(
f"Resource not found: {response.resource_not_found.resource_id} "
f"- {response.resource_not_found.message}"
)
elif result_field == "exception_message":
raise RuntimeError(f"Server error: {response.exception_message}")
Check project-level access:
response = client.IsAllowedProject(
pb.AccessProjectRequest(
user_id=user_id,
access_level="write",
project_id=project_id,
)
)
if response.WhichOneof("result") != "success":
raise PermissionError("Access denied")
Get user's role in a project:
response = client.IsProjectUserRole(
pb.RoleProjectRequest(
user_id=user_id,
project_id=project_id,
)
)
result_field = response.WhichOneof("result")
if result_field == "role":
print(f"Role: {response.role.role}") # "owner", "admin", ...
print(f"Project: {response.role.project_name}")
else:
print(f"Error: {response.error}")
Get user's scope in a company:
response = client.IsCompanyUserRole(
pb.CompanyProjectRequest(
user_id=user_id,
company_id=company_id,
)
)
result_field = response.WhichOneof("result")
if result_field == "role":
print(f"Scope: {response.role.role}") # "owner", "admin", "editor", ...
else:
print(f"Error: {response.error}")
List available projects:
response = client.GetAvailableProject(
pb.UserRequest(user_id=user_id)
)
for project in response.projects:
print(f"{project.project_name} ({project.role})")
List available companies:
response = client.GetAvailableCompanies(
pb.UserRequest(user_id=user_id)
)
for company in response.companies:
print(f"{company.company_name} ({company.role})")
Async Python (grpcio with asyncio)¶
import grpc.aio
channel = grpc.aio.insecure_channel(POLICY_VAULT_URL)
client = pb_grpc.AccessResourceServiceStub(channel)
response = await client.IsAllowed(
pb.AccessResourceRequest(
user_id=user_id,
access_level="read",
project_id=project_id,
resource_id=pb.ResourceIdValue(
file_asset=pb.BinaryAssetIdValue(single_file=file_path)
),
)
)
Constructing resource IDs¶
Examples by resource type¶
Single file:
| Language | Code |
|---|---|
| Node.js | { fileAsset: { singleFile: "models/v2/weights.bin" } } |
| Python | pb.ResourceIdValue(file_asset=pb.BinaryAssetIdValue(single_file="models/v2/weights.bin")) |
Folder:
| Language | Code |
|---|---|
| Node.js | { fileAsset: { folder: "datasets/training/" } } |
| Python | pb.ResourceIdValue(file_asset=pb.BinaryAssetIdValue(folder="datasets/training/")) |
Template:
| Language | Code |
|---|---|
| Node.js | { fileAsset: { template: "default-pipeline" } } |
| Python | pb.ResourceIdValue(file_asset=pb.BinaryAssetIdValue(template="default-pipeline")) |
Generic string resource:
| Language | Code |
|---|---|
| Node.js | { common: "some-resource-id" } |
| Python | pb.ResourceIdValue(common="some-resource-id") |
Reading oneof responses¶
The AccessResourceResponse, ProjectRoleResponse, and CompanyRoleResponse messages use protobuf oneof fields. To determine which field is set:
Node.js (with oneofs: true in proto-loader options):
The response object gets a virtual field with the name of the oneof group (result), whose value is the name of the field that is currently set:
switch (response.result) {
case "success":
// access granted
break;
case "accessDeniedMessage":
// response.accessDeniedMessage contains the reason
break;
case "userNotFoundId":
// response.userNotFoundId contains the UUID
break;
case "resourceNotFound":
// response.resourceNotFound.resourceId, response.resourceNotFound.message
break;
case "exceptionMessage":
// response.exceptionMessage contains the error
break;
}
Python:
Use WhichOneof("result") to get the name of the set field:
field = response.WhichOneof("result")
# field is one of: "success", "user_not_found_id", "resource_not_found",
# "access_denied_message", "exception_message"
Error handling¶
| gRPC status code | Cause |
|---|---|
INVALID_ARGUMENT (3) |
Malformed user_id (not a valid UUID), unknown access_level, or invalid resource_id |
NOT_FOUND (5) |
User not found (only for GetAvailableProject and GetAvailableCompanies) |
OK (0) with access_denied_message set |
User exists but lacks required permissions |
OK (0) with user_not_found_id set |
User not found (for IsAllowed / IsAllowedProject) |
OK (0) with exception_message set |
Unexpected server-side error |
Important: IsAllowed and IsAllowedProject return access denials as a successful gRPC response (status OK) with the appropriate oneof field set — not as a gRPC error. Always check the oneof discriminator (result in Node.js, WhichOneof("result") in Python) to determine the outcome.
Handling gRPC errors:
Node.js:
try {
const res = await accessClient.getAvailableProject(request, metadata);
} catch (err) {
if (err.code === grpc.status.NOT_FOUND) {
// user does not exist
} else if (err.code === grpc.status.INVALID_ARGUMENT) {
// bad request parameters
}
}
Python:
try:
response = client.GetAvailableProject(pb.UserRequest(user_id=user_id))
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.NOT_FOUND:
# user does not exist
pass
elif e.code() == grpc.StatusCode.INVALID_ARGUMENT:
# bad request parameters
pass
s
Method selection guide¶
Use this table to select the appropriate authorization method based on scope and intent. Prefer IsAllowed for resource-level access checks, project/company role methods for contextual authorization, and GetAvailable* helpers when resolving visibility or filtering entities. This keeps permission logic consistent and avoids duplicating access rules.
| Scenario | Method |
|---|---|
| "Can the user read/write/admin this file?" | IsAllowed |
| "Can the user perform this action in this project?" | IsAllowedProject |
| "What is the user's role in this project?" | IsProjectUserRole |
| "What is the user's scope in this company?" | IsCompanyUserRole |
| "Which projects can this user see?" | GetAvailableProject |
| "Which companies does this user belong to?" | GetAvailableCompanies |