Skip to content

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