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:manageTwo ways to split the work
Section titled “Two ways to split the work”| 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.
Ask before you act
Section titled “Ask before you act”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.
Maintain entries
Section titled “Maintain entries”| 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: falseand oneuserentry per member is a common pattern. - A
roleentry names a role id fromroles(), nevertenant_owner(who holds everything anyway). - An entry can carry
expires_atfor 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_foundotherwise).