Skip to content

Permissions from code

Your backend’s machine client may do anything in its application, so the File Service’s permission model does not limit it. It can still use the model: ask what a user may do and enforce the answer, and maintain the entries that decide it. This guide is for backend developers; the model itself is on Permissions.

Machine scope: files:manage
Your backend decides The service decides
Your own rules (author of the post, member of the team) run before every call; files get visibility: 'application' or stay private to the owner Folders carry entries for users and roles; the service answers for each user
Members see what your API hands them; owner_actions can be view, download so only your backend writes Your app may call the service as the user (User tokens), and the service enforces
Typical for a social app Typical for a document library

Both can mix: a social app’s backend may still ask the service before it hands out a private file.

permissions.ts
import type { FileAction } from '@central-code-station/ccs-files-client';
import { files } from './files-client';
// Your backend holds files:manage, so the entries never limit it. Before it acts for a user, it asks the service
// what that user may do, and enforces the answer itself.
export async function downloadableBy(userId: string, fileIds: string[]): Promise<string[]> {
const { items } = await files.files.effectivePermissionsBatch(userId, fileIds); // up to 200 ids
return items.filter((item) => item.actions.includes('download')).map((item) => item.file_id);
}
export async function explain(fileId: string, userId: string): Promise<string> {
const { actions, via } = await files.files.effectivePermissions(fileId, userId);
return `${userId} may ${actions.join(', ') || 'nothing'} (through ${via.join(', ') || 'no grant'})`;
}
// Give one user access to a folder and everything inside it that inherits.
export async function invite(folderId: string, userId: string, actions: FileAction[] = ['view', 'download']) {
return files.permissions.grant({ folderId }, { principal: { type: 'user', user_id: userId }, actions });
}
// Make a folder private to its own entries: copy what it inherits today, then stop inheriting.
export async function breakInheritance(folderId: string) {
return files.permissions.replace({ folderId }, { inherit: false, copy_inherited: true, entries: [] });
}
Route Answers
GET P/files/:file_id/effective-permissions { user_id, actions, via } for one file and one user. via says where each grant came from: system:files.view, role:tenant_owner, owner, visibility:application, role:editor@folder:<id>, user@item
POST P/files/effective-permissions { items: [{ file_id, actions }] } for up to 200 files and one user

The answer is the same the service would give that user’s own token, including the user’s roles at the Identity Server, the owner rights and inheritance. Use the batch form to filter a page before minting its URLs.

Call Route
files.permissions.get({ folderId }) GET P/folders/:folder_id/permissions
files.permissions.grant(item, entry) POST P/folders/:folder_id/permissions/entries, POST P/files/:file_id/permissions/entries
files.permissions.revoke(item, entryId) DELETE P/folders/:folder_id/permissions/entries/:entry_id, DELETE P/files/:file_id/permissions/entries/:entry_id
files.permissions.replace(item, { inherit, copy_inherited, keep_own, entries }) PUT P/folders/:folder_id/permissions, PUT P/files/:file_id/permissions
files.permissions.roles() GET P/principals/roles: the roles an entry may name
  • Prefer entries on folders: one entry covers every file that inherits, and membership changes cost one call per member, never one per file. A team folder with inherit: false and one user entry per member is a common pattern.
  • A role entry names a role id from roles(), never tenant_owner (who holds everything anyway).
  • An entry can carry expires_at for temporary access.
  • An item holds at most 200 entries of its own (too_many_entries); revoking an inherited entry is done on the folder that holds it (entry_not_found otherwise).