API: Права доступа и группы
Пошаговый гайд для разработчиков расширений (C# SDK: check, Grant, группы, AD):
Права доступа из кода.
Права — /api/v1/permissions
Заголовок раздела «Права — /api/v1/permissions»Требуется авторизация.
Типы ресурсов
Заголовок раздела «Типы ресурсов»| resourceType | Наследование |
|---|---|
node | Родительские узлы до корня |
list | Список → узел → предки узла |
list_item | Элемент → список → узел → предки (или только элемент при inherits_permissions = false) |
library | Библиотека → узел → предки |
page | Страница → узел → предки |
survey | Опрос → узел → предки |
file | Файл/папка → родительские папки → библиотека → узел → предки |
attachment | Вложение → элемент → список → узел → предки |
Идентификаторы ресурсов
Заголовок раздела «Идентификаторы ресурсов»| resourceType | resourceId | Доп. параметры |
|---|---|---|
node | UUID узла | — |
list | UUID списка | — |
library | UUID библиотеки | — |
page | UUID страницы | — |
survey | UUID опроса | — |
list_item | UUID элемента или числовой id (порядковый номер в списке) | listId — обязателен при числовом resourceId |
file | UUID файла/папки или числовой id | libraryId — обязателен при числовом resourceId |
attachment | UUID вложения | — |
Внутри БД ACL всегда хранится по UUID. REST принимает публичный числовой id элементов и файлов, если передан контекст списка/библиотеки.
GET /permissions?resourceType={type}&resourceId={id}&listId={uuid}&libraryId={uuid}
Заголовок раздела «GET /permissions?resourceType={type}&resourceId={id}&listId={uuid}&libraryId={uuid}»Список явных назначений на ресурс (без унаследованных). Требуется право edit на ресурс.
Примеры:
GET /api/v1/permissions?resourceType=node&resourceId=550e8400-e29b-41d4-a716-446655440000GET /api/v1/permissions?resourceType=list_item&resourceId=12&listId=550e8400-e29b-41d4-a716-446655440000GET /api/v1/permissions?resourceType=file&resourceId=5&libraryId=660e8400-e29b-41d4-a716-446655440001POST /permissions
Заголовок раздела «POST /permissions»Добавить или обновить права (upsert по паре principal + resource). Требуется право edit на ресурс. Для list, library и survey — только администратор портала.
Тело запроса:
{ "resourceType": "list", "resourceId": "550e8400-e29b-41d4-a716-446655440000", "principalType": "group", "principalId": "770e8400-e29b-41d4-a716-446655440002", "permissionLevel": "view"}Для элемента списка с числовым id:
{ "resourceType": "list_item", "resourceId": "12", "listId": "550e8400-e29b-41d4-a716-446655440000", "principalType": "user", "principalId": "880e8400-e29b-41d4-a716-446655440003", "permissionLevel": "edit"}Для файла/папки:
{ "resourceType": "file", "resourceId": "5", "libraryId": "660e8400-e29b-41d4-a716-446655440001", "principalType": "group", "principalId": "770e8400-e29b-41d4-a716-446655440002", "permissionLevel": "view"}Уровни: view, add, edit, delete (каждый следующий включает предыдущие). В интерфейсе delete отображается как Полный доступ.
principalType: user или group.
В панели прав UI — один чип-пикер: несколько пользователей и групп сразу, один уровень на всех выбранных. Поиск субъектов — GET /users?q= и GET /groups?q= (рядом с селектом уровня — справка ?).
DELETE /permissions/:id
Заголовок раздела «DELETE /permissions/:id»Удалить права по UUID записи ACL. Требуется право edit на соответствующий ресурс.
GET /permissions/check?resourceType={type}&resourceId={id}&listId={uuid}&libraryId={uuid}
Заголовок раздела «GET /permissions/check?resourceType={type}&resourceId={id}&listId={uuid}&libraryId={uuid}»Эффективные права текущего пользователя с учётом наследования по цепочке ресурсов и членства в группах.
Ответ 200:
{ "success": true, "data": { "permissionLevel": "edit", "canView": true, "canAdd": true, "canEdit": true, "canDelete": false }}Если доступа нет, permissionLevel будет null, все флаги — false.
Группы — /api/v1/groups
Заголовок раздела «Группы — /api/v1/groups»Только portal_admin. Управление группами в UI: Группы доступа.
Типы групп
Заголовок раздела «Типы групп»group_type | Описание | CRUD в UI / API |
|---|---|---|
portal | Создана вручную в портале | Полный CRUD и управление участниками |
domain_linked | Импортирована из AD (ad-sync) | Только чтение; изменение состава — через ad-sync |
Для domain_linked запросы PATCH, DELETE, POST/DELETE .../members возвращают ошибку валидации: «Состав группы управляется синхронизацией AD (ad-sync)» (для update/delete группы — аналогично).
Эндпоинты
Заголовок раздела «Эндпоинты»| Метод | Путь | Описание |
|---|---|---|
| GET | /groups | Список всех групп (id, name, description, group_type, members_count) |
| GET | /groups?q=... | Поиск по названию/описанию (до 20, для picker’ов назначения прав) |
| POST | /groups | Создать группу portal |
| GET | /groups/:id | Группа с массивом members |
| PATCH | /groups/:id | Обновить name и/или description (только portal) |
| DELETE | /groups/:id | Удалить группу (только portal) |
| POST | /groups/:id/members | Добавить участника (только portal) |
| DELETE | /groups/:id/members/:userId | Удалить участника (только portal) |
Создание и обновление
Заголовок раздела «Создание и обновление»POST /api/v1/groupsContent-Type: application/json
{ "name": "Редакторы HR", "description": "Редактирование раздела HR" }PATCH /api/v1/groups/{id}Content-Type: application/json
{ "name": "Новое название", "description": "Обновлённое описание" }Участники
Заголовок раздела «Участники»По существующему пользователю портала:
POST /api/v1/groups/{id}/membersContent-Type: application/json
{ "userId": "uuid" }Upsert доменного пользователя из каталога (создаётся запись в users при необходимости):
POST /api/v1/groups/{id}/membersContent-Type: application/json
{ "externalId": "object-guid-from-ad", "login": "ivanov", "displayName": "Иванов Иван", "email": "ivanov@company.local"}Удаление участника:
DELETE /api/v1/groups/{id}/members/{userId}В группу добавляются пользователи, не вложенные группы. AD-группы как отдельные сущности создаются только через ad-sync (см. API: admin).
Ответ GET /groups/:id
Заголовок раздела «Ответ GET /groups/:id»{ "success": true, "data": { "id": "uuid", "name": "Редакторы HR", "description": "", "group_type": "portal", "members": [ { "id": "user-uuid", "login": "petrov", "display_name": "Петров Пётр", "auth_source": "domain" } ] }}При удалении группы инвалидируется кэш прав; назначения principalType: "group" на эту группу перестают действовать.
Пользователи — GET /api/v1/users?q=
Заголовок раздела «Пользователи — GET /api/v1/users?q=»Список пользователей для назначения прав (только admin).
Карточка сотрудника (любой аутентифицированный)
Заголовок раздела «Карточка сотрудника (любой аутентифицированный)»| Метод | Путь | Описание |
|---|---|---|
| GET | /users/{id}/profile | Профиль: имя, email, card_fields (meta по personCardVisibleFields в ad-sync), has_avatar, manager_user_id |
| GET | /users/{id}/avatar | Фото из объектного хранилища (image/jpeg) или 404 |
| GET | /users/{id}/org-chain | Цепочка руководителей от корня до пользователя (chain[]) |
| GET | /users/{id}/org-children | Прямые подчинённые для lazy-раскрытия (children[]) |
Узлы оргструктуры: id, display_name, login, title, has_avatar, has_children.
JS (PortalApi.users)
Заголовок раздела «JS (PortalApi.users)»const profile = await PortalApi.users.getProfile(userId);const chain = await PortalApi.users.getOrgChain(userId);const children = await PortalApi.users.getOrgChildren(userId);const imgSrc = PortalApi.users.avatarUrl(userId); // /api/v1/users/{id}/avatar- WebPart:
ctx.Users.GetProfileAsync/GetOrgChainAsync/GetOrgChildrenAsync/AvatarUrl— см. WebPart SDK - Event Receiver:
api.Users.*— см. Event Receiver SDK - Timer Job:
api.Users.*— см. Timer Job SDK
Типы ответа: Portal.Contracts.Users (UserPublicProfile, UserOrgNode, UserOrgChain, UserOrgChildren).
Наследование
Заголовок раздела «Наследование»По умолчанию дочерние ресурсы наследуют права от родителя. При проверке обходим цепочку от конкретного ресурса к корню и берём максимальный уровень среди всех найденных назначений. Явные права на дочернем ресурсе дополняют унаследованные, а не заменяют их.
Для файлов и папок библиотеки цепочка включает все родительские папки. Если у папки или файла отключено наследование (inherits_permissions = false), объект становится границей: права библиотеки и узла выше границы не применяются к нему и его потомкам. При отключении наследования копируются эффективные права родителя как явные назначения.
Для элементов списка поведение аналогично: по умолчанию inherits_permissions = true, цепочка list_item → list → node → …. При отключении наследования элемент становится границей — права списка и узла не применяются; при снятии флага копируются эффективные права списка (и узла) как явные назначения на list_item. Управление флагом — PATCH /lists/{listId}/items/{itemRef}/permission-settings (только администратор портала). Явные права на элемент — через /api/v1/permissions с resourceType=list_item и listId.
Настройки наследования (элементы и документы)
Заголовок раздела «Настройки наследования (элементы и документы)»Только администратор портала.
GET /api/v1/lists/{listId}/items/{itemRef}/permission-settingsPATCH /api/v1/lists/{listId}/items/{itemRef}/permission-settings Content-Type: application/json { "inheritFromParent": false }
GET /api/v1/libraries/{libraryId}/files/{fileRef}/permission-settingsPATCH /api/v1/libraries/{libraryId}/files/{fileRef}/permission-settings { "inheritFromParent": false }itemRef / fileRef — UUID или числовой id. При inheritFromParent: false копируются эффективные права родителя; при true — удаляются уникальные назначения на ресурсе.
Примеры из кода
Заголовок раздела «Примеры из кода»Ниже — типовые сценарии проверки и назначения прав для всех типов ресурсов. Уровни: view < add < edit < delete (в UI «Полный доступ»). Администратор портала (is_portal_admin) всегда получает delete на любой ресурс.
Кто может назначать права
Заголовок раздела «Кто может назначать права»| resourceType | Кто может вызывать POST/DELETE /permissions |
|---|---|
node, page | Пользователь с edit на ресурс |
list, library, survey, list_item, file | Только администратор портала (плюс edit на ресурс) |
attachment | Пользователь с edit на вложение |
REST: проверка прав
Заголовок раздела «REST: проверка прав»# Узелcurl -s -b cookies.txt \ "$BASE/api/v1/permissions/check?resourceType=node&resourceId=$NODE_ID"
# Списокcurl -s -b cookies.txt \ "$BASE/api/v1/permissions/check?resourceType=list&resourceId=$LIST_ID"
# Элемент списка (числовой id)curl -s -b cookies.txt \ "$BASE/api/v1/permissions/check?resourceType=list_item&resourceId=12&listId=$LIST_ID"
# Библиотекаcurl -s -b cookies.txt \ "$BASE/api/v1/permissions/check?resourceType=library&resourceId=$LIBRARY_ID"
# Файл / папкаcurl -s -b cookies.txt \ "$BASE/api/v1/permissions/check?resourceType=file&resourceId=5&libraryId=$LIBRARY_ID"
# Страницаcurl -s -b cookies.txt \ "$BASE/api/v1/permissions/check?resourceType=page&resourceId=$PAGE_ID"
# Вложение к элементу спискаcurl -s -b cookies.txt \ "$BASE/api/v1/permissions/check?resourceType=attachment&resourceId=$ATTACHMENT_ID"Ответ при достаточных правах:
{ "success": true, "data": { "permissionLevel": "edit", "canView": true, "canAdd": true, "canEdit": true, "canDelete": false }}Если доступа нет: permissionLevel — null, все флаги false.
REST: назначение и отзыв
Заголовок раздела «REST: назначение и отзыв»# Выдать группе «Редактирование» на узелcurl -s -b cookies.txt -X POST "$BASE/api/v1/permissions" \ -H 'Content-Type: application/json' \ -d '{ "resourceType": "node", "resourceId": "'"$NODE_ID"'", "principalType": "group", "principalId": "'"$GROUP_ID"'", "permissionLevel": "edit" }'
# Список явных назначений на страницуcurl -s -b cookies.txt \ "$BASE/api/v1/permissions?resourceType=page&resourceId=$PAGE_ID"
# Удалить праваcurl -s -b cookies.txt -X DELETE "$BASE/api/v1/permissions/$PERMISSION_ID"Поиск пользователей и групп для picker’ов (только admin):
GET /api/v1/users?q=ивановGET /api/v1/groups?q=hrJavaScript (фронтенд, WebPart)
Заголовок раздела «JavaScript (фронтенд, WebPart)»Проверка через PortalApi (как в listDetailView.js, permissionsPanel.js):
async function checkPermissions(resourceType, resourceId, context = {}) { const params = new URLSearchParams({ resourceType, resourceId: String(resourceId), }); if (resourceType === 'list_item' && context.listId) { params.set('listId', String(context.listId)); } if (resourceType === 'file' && context.libraryId) { params.set('libraryId', String(context.libraryId)); } const res = await PortalApi.get(`/permissions/check?${params}`); return res.data; // { permissionLevel, canView, canAdd, canEdit, canDelete }}
// Узелconst nodePerms = await checkPermissions('node', nodeId);if (!nodePerms.canEdit) return;
// Элемент списка (удобнее брать item.permissions из GET /lists/{id}/items)const itemPerms = await checkPermissions('list_item', 12, { listId });if (itemPerms.canEdit) { /* редактирование */ }
// Файлconst filePerms = await checkPermissions('file', 5, { libraryId });Добавить права:
await PortalApi.post('/permissions', { resourceType: 'list_item', resourceId: String(itemId), listId, principalType: 'group', principalId: groupId, permissionLevel: 'view',});
// Список явных назначенийconst query = new URLSearchParams({ resourceType: 'file', resourceId: String(fileId), libraryId,});const list = await PortalApi.get(`/permissions?${query}`);Панель прав в UI: PortalPermissionsPanel.render(resourceType, resourceId, $container, { listId, libraryId }) — см. frontend/public/js/permissions/permissionsPanel.js. Форма назначения использует единый multi-пикер пользователей и групп (PortalAclPrincipalPicker).
WebPart (portalContext.js) — упрощённая проверка для node, list, library, page (без listId/libraryId):
const ctx = PortalWebPartContext.create({ nodeId, api: PortalApi });const canEditList = await ctx.permissions.check('list', listId, 'edit');// level: 'view' | 'add' | 'edit' | 'delete'Для list_item и file в WebPart вызывайте PortalApi.get с listId / libraryId напрямую (см. пример checkPermissions выше).
C# SDK (WebPart / Event Receiver / Timer Job)
Заголовок раздела «C# SDK (WebPart / Event Receiver / Timer Job)»Рекомендуемый путь в расширениях — ctx.Permissions / api.Permissions (те же серверные правила ACL).
using Portal.Contracts.Permissions;
// WebPartvar listId = ctx.Properties.GetGuid("listId"); // или FindBySlug / ResolvePath — см. гайдvar node = await ctx.Permissions.ForCurrentNodeAsync(ct);var list = await ctx.Permissions.ForListAsync(listId, ct);var item = await ctx.Permissions.ForListItemAsync(listId, "12", ct);
var editors = await ctx.Groups.FindByNameAsync("Редакторы HR", ct);await ctx.Permissions.GrantToGroupAsync( PermissionResource.ListItem, "12", editors!.Id, PermissionLevel.Edit, listId: listId, ct: ct);
var acl = await ctx.Permissions.ListAsync(PermissionResource.ListItem, "12", listId: listId, ct: ct);await ctx.Permissions.RevokeAsync(acl[0].Id, ct);
// Event Receiver / Timer Job — тот же API: api.Permissions.ForListAsync(…)| Тип | Описание |
|---|---|
PortalPermission | Level, CanView / CanAdd / CanEdit / CanDelete, Has(level) |
PortalAclEntry | Явная запись ACL (Id, PrincipalType, PrincipalId, PermissionLevel, …) |
PermissionResource | Константы Node, List, ListItem, Library, Page, Survey, File, Attachment |
Кто может назначать — см. таблицу выше. На list / library / survey — только portal admin; Timer Job идёт от portal-system (admin) и может менять ACL.
C# (бэкенд: сервисы внутри Portal.Application)
Заголовок раздела «C# (бэкенд: сервисы внутри Portal.Application)»Инжектируйте PermissionService (проверка) и PermissionAdminService (управление ACL), если пишете код платформы, а не extension SDK.
using Portal.Contracts.Permissions;using Portal.Application.Permissions;
var canEdit = await permissionService.HasPermissionAsync( user, "list_item", itemGuid, PermissionLevel.Edit, ct);
await permissionAdmin.UpsertAsync(user, new UpsertPermissionRequest{ ResourceType = "list_item", ResourceId = "12", ListId = listId, PrincipalType = "user", PrincipalId = userId, PermissionLevel = "view",}, ct);После прямых изменений в PermissionRepository вызовите permissionService.InvalidatePermissionCacheAsync().
Сводка: что проверять для операций
Заголовок раздела «Сводка: что проверять для операций»| Операция | Минимальный уровень | resourceType |
|---|---|---|
| Открыть узел / список / библиотеку | view | node, list, library |
| Создать элемент списка | add на list | list |
| Изменить элемент | edit на list_item (и view на list) | list_item |
| Удалить элемент | delete на list_item | list_item |
| Загрузить файл | add на library или родительскую папку | library / file |
| Скачать / открыть документ | view на file | file |
| Редактировать страницу | edit на page | page |
| Создать опрос | edit на node | node |
| Пройти открытый опрос | view на survey (и view на узел) | survey |
| Конструктор / результаты опроса | edit на survey | survey |
| Управлять ACL ресурса | edit на ресурс (+ admin для list/library/survey/list_item/file) | соответствующий тип |
Не путайте права на список (list) и на строку (list_item): для конфиденциальной записи назначайте ACL на list_item или отключайте наследование элемента от списка.