Права доступа из кода
Как из расширения (WebPart / Event Receiver / Timer Job) проверить права пользователя, назначить ACL и узнать, состоит ли пользователь в группе портала или в группе из AD.
Полный REST-справочник: API: права и группы.
Для конечных пользователей (UI): Права доступа.
Что получится
Заголовок раздела «Что получится»- Понять, откуда взять
groupId/listId/nodeId(Property Pane, имя, slug, URL). - Проверить эффективные права на узел / список / элемент / файл (
CanView…CanDelete). - Выдать или отозвать права пользователю или группе.
- Проверить членство в группе и отличить группу из AD (
domain_linked).
0. Базовые понятия
Заголовок раздела «0. Базовые понятия»| Уровень | API | Что можно |
|---|---|---|
| Просмотр | view | Читать ресурс |
| Добавление | add | + создавать |
| Редактирование | edit | + изменять |
| Полный доступ | delete | + удалять |
Каждый следующий уровень включает предыдущие. В C#: enum PermissionLevel (View / Add / Edit / Delete).
Типы ресурсов
Заголовок раздела «Типы ресурсов»| Ресурс | PermissionResource / API | Как указать id |
|---|---|---|
| Узел | Node / node | UUID |
| Список | List / list | UUID |
| Элемент списка | ListItem / list_item | UUID или номер + listId |
| Библиотека | Library / library | UUID |
| Файл / папка | File / file | UUID или номер + libraryId |
| Страница | Page / page | UUID |
| Вложение | Attachment / attachment | UUID |
Эффективные права считаются с наследованием (элемент → список → узел → …) и с учётом членства в группах. Администратор портала (IsPortalAdmin) всегда имеет полный доступ.
Откуда берётся «текущий пользователь»
Заголовок раздела «Откуда берётся «текущий пользователь»»| Расширение | Кто проверяется / от чьего имени ACL |
|---|---|
| WebPart | Пользователь страницы (ctx.User) |
| Event Receiver | Пользователь события (context.User → api.Permissions) |
| Timer Job | Системная учётка portal-system (portal admin) |
1. Откуда брать groupId, listId, nodeId, …
Заголовок раздела «1. Откуда брать groupId, listId, nodeId, …»UUID не нужно копировать из админки вручную в каждый пример. В WebPart SDK есть резолв по имени, slug и URL/path.
Рекомендуемый способ: Property Pane
Заголовок раздела «Рекомендуемый способ: Property Pane»Администратор выбирает список/узел/группу в настройках WebPart → в коде читаете UUID:
var listId = ctx.Properties.GetGuid("listId"); // listPickervar nodeId = ctx.Properties.GetGuid("sourceNodeId"); // nodePickervar groupId = ctx.Properties.GetGuid("editorsGroup"); // если добавите groupPicker / text с UUIDСценарии picker’ов: Параметры WebPart.
Группа по имени / поиск
Заголовок раздела «Группа по имени / поиск»// Точное имя (как в Админка → Группы доступа)var editors = await ctx.Groups.FindByNameAsync("Редакторы HR", ct) ?? throw new InvalidOperationException("Группа не найдена");
await ctx.Permissions.GrantToGroupAsync( PermissionResource.ListItem, "12", editors.Id, PermissionLevel.Edit, listId: listId, ct: ct);
// Поиск (подстрока)var found = await ctx.Groups.SearchAsync("HR", limit: 10, ct: ct);var groupId = found[0].Id;
// Карточка + тип (portal / domain_linked из AD)var info = await ctx.Groups.GetAsync(groupId, ct);bool fromAd = info?.IsFromDirectory == true; // или info.GroupType == "domain_linked"Список / библиотека по slug или названию в узле
Заголовок раздела «Список / библиотека по slug или названию в узле»var nodeId = ctx.NodeId ?? (await ctx.Nodes.FindBySlugAsync("portal", parentId: null, ct))!.Id;
// slug из URL: /portal/hr/lists/tasks → slug списка = "tasks"var list = await ctx.Lists.FindBySlugAsync(nodeId, "tasks", ct) ?? await ctx.Lists.FindByTitleAsync(nodeId, "Задачи", ct);
var library = await ctx.Libraries.FindBySlugAsync(nodeId, "docs", ct) ?? await ctx.Libraries.FindByTitleAsync(nodeId, "Документы", ct);
Guid listId = list!.Id;Узел по slug (цепочка родителей)
Заголовок раздела «Узел по slug (цепочка родителей)»var portal = await ctx.Nodes.FindBySlugAsync("portal", parentId: null, ct);var hr = await ctx.Nodes.FindBySlugAsync("hr", parentId: portal!.Id, ct);Path или полный URL → сразу все id
Заголовок раздела «Path или полный URL → сразу все id»// Канонический path или URL вида https://portal.example/portal/hr/lists/tasksvar route = await ctx.Routes.ResolvePathAsync("/portal/hr/lists/tasks", ct);// route.Type == "list", route.NodeId, route.ListId
var itemRoute = await ctx.Routes.ResolvePathAsync( "https://portal.example/portal/hr/lists/tasks/items/12", ct);// itemRoute.ListId, itemRoute.ItemNumber == 12
if (route.ListId is Guid lid){ await ctx.Permissions.ForListAsync(lid, ct);}Модуль (.portalmod)
Заголовок раздела «Модуль (.portalmod)»После установки UUID лежат в resourceMap / refs provisioner (lists.tasks, groups.itStaff). Их можно прокинуть в Property Pane по умолчанию или читать из настроек списка модуля.
Сводка методов резолва (WebPart)
Заголовок раздела «Сводка методов резолва (WebPart)»| Нужен | Метод |
|---|---|
| Группа по имени | ctx.Groups.FindByNameAsync("…") → .Id |
| Группы по поиску | ctx.Groups.SearchAsync("…") |
| Тип группы (AD?) | ctx.Groups.GetAsync(id) → GroupType / IsFromDirectory |
| Список по slug/названию | ctx.Lists.FindBySlugAsync(nodeId, "tasks") / FindByTitleAsync |
| Библиотека | ctx.Libraries.FindBySlugAsync / FindByTitleAsync |
| Узел по slug | ctx.Nodes.FindBySlugAsync("hr", parentId) |
| Всё из URL/path | ctx.Routes.ResolvePathAsync("/portal/…") |
| Список из настроек WebPart | ctx.Properties.GetGuid("listId") |
| Текущий узел страницы | ctx.NodeId |
2. Проверить права (C# SDK)
Заголовок раздела «2. Проверить права (C# SDK)»Подключите using Portal.Contracts.Permissions;.
WebPart
Заголовок раздела «WebPart»// Текущий узел страницыvar node = await ctx.Permissions.ForCurrentNodeAsync(ct);if (!node.CanView) return new WebPartResult("", "Нет доступа к разделу");
// Списокvar list = await ctx.Permissions.ForListAsync(listId, ct);if (!list.CanAdd) { /* скрыть кнопку «Создать» */ }
// Элемент (номер или UUID)var item = await ctx.Permissions.ForListItemAsync(listId, "12", ct);if (item.CanEdit) { /* форма правки */ }if (item.CanDelete) { /* кнопка удаления */ }
// Файлvar file = await ctx.Permissions.ForFileAsync(libraryId, "5", ct);
// Универсальноif (await ctx.Permissions.CanAsync( PermissionResource.List, listId.ToString(), PermissionLevel.Edit, ct: ct)){ // …}Результат — PortalPermission:
| Свойство | Тип | Смысл |
|---|---|---|
Level | PermissionLevel? | Эффективный уровень (null = нет доступа) |
CanView / CanAdd / CanEdit / CanDelete | bool | Флаги по иерархии |
Has(required) | bool | Достаточно ли уровня required |
Event Receiver / Timer Job
Заголовок раздела «Event Receiver / Timer Job»Тот же API на api.Permissions (без ForCurrentNodeAsync):
var item = await api.Permissions.ForListItemAsync(listId, itemRef, ct);if (!item.CanEdit) return new { skipped = true, reason = "no edit" };Краткий справочник методов check
Заголовок раздела «Краткий справочник методов check»| Метод | Что проверяет |
|---|---|
ForCurrentNodeAsync | Узел текущей страницы (только WebPart; без NodeId → все флаги false) |
ForNodeAsync(nodeId) | Узел |
ForListAsync(listId) | Список |
ForListItemAsync(listId, itemRef) | Элемент |
ForLibraryAsync / ForFileAsync | Библиотека / файл |
ForPageAsync(pageId) | Страница |
CheckAsync(resourceType, resourceId, listId?, libraryId?) | Любой тип строкой API |
CanAsync(..., required) | true/false для нужного уровня |
JS на странице (дополнительно)
Заголовок раздела «JS на странице (дополнительно)»const canEdit = await ctx.permissions.check('list', listId, 'edit');// list_item / file:const res = await PortalApi.get( `/permissions/check?resourceType=list_item&resourceId=12&listId=${listId}`);// res.data: { permissionLevel, canView, canAdd, canEdit, canDelete }3. Назначить и отозвать права (C# SDK)
Заголовок раздела «3. Назначить и отозвать права (C# SDK)»Кто может назначать
Заголовок раздела «Кто может назначать»| Ресурс | Кто может Grant / Revoke / List явных ACL |
|---|---|
node, page | Пользователь с правом edit на ресурс |
list, library | Только администратор портала |
list_item, file | Нужен edit на ресурс; для list/library-правил см. API |
| Timer Job | Идёт как portal-system (admin) — может менять ACL |
Выдать права
Заголовок раздела «Выдать права»using Portal.Contracts.Permissions;
var readers = await ctx.Groups.FindByNameAsync("Читатели портала", ct) ?? throw new InvalidOperationException("Группа не найдена");
// Группе — просмотр узлаawait ctx.Permissions.GrantToGroupAsync( PermissionResource.Node, ctx.NodeId!.Value.ToString(), readers.Id, PermissionLevel.View, ct: ct);
// Пользователю — правка элемента списка (номер + listId)await ctx.Permissions.GrantToUserAsync( PermissionResource.ListItem, "12", userId, PermissionLevel.Edit, listId: listId, ct: ct);
// Файлу — группаawait ctx.Permissions.GrantToGroupAsync( PermissionResource.File, "5", editorsGroupId, PermissionLevel.View, libraryId: libraryId, ct: ct);
// Универсально (principalType: "user" | "group")await ctx.Permissions.GrantAsync( PermissionResource.Page, pageId.ToString(), "group", groupId, PermissionLevel.Edit, ct: ct);Grant* делает upsert: повторный вызов для той же пары (ресурс + principal) обновляет уровень.
Возвращает PortalAclEntry (Id, PrincipalType, PrincipalId, PermissionLevel, PrincipalName, …).
Список явных назначений и отзыв
Заголовок раздела «Список явных назначений и отзыв»var acl = await ctx.Permissions.ListAsync( PermissionResource.ListItem, "12", listId: listId, ct: ct);
foreach (var entry in acl){ // entry.PrincipalType, entry.PrincipalId, entry.PermissionLevel, entry.PrincipalName}
await ctx.Permissions.RevokeAsync(acl[0].Id, ct);ListAsync возвращает только явные записи на ресурс (без унаследованных). Для вызова нужно право edit на ресурс (и admin-правила для list/library).
При установке модуля
Заголовок раздела «При установке модуля»Декларативно в module.json → массив permissions (при install/upgrade). См. Функциональные модули.
4. Группы портала: состоит ли пользователь
Заголовок раздела «4. Группы портала: состоит ли пользователь»Быстро: группы уже в контексте WebPart
Заголовок раздела «Быстро: группы уже в контексте WebPart»При рендере WebPart платформа кладёт id групп текущего пользователя в ctx.User.GroupIds:
var itStaff = await ctx.Groups.FindByNameAsync("Сотрудники IT", ct);bool inGroup = itStaff is not null && ctx.User!.GroupIds.Contains(itStaff.Id);bool isAdmin = ctx.User!.IsPortalAdmin;Явная проверка через SDK
Заголовок раздела «Явная проверка через SDK»var group = await ctx.Groups.FindByNameAsync("Редакторы HR", ct) ?? throw new InvalidOperationException("Группа не найдена");
bool member = await ctx.Groups.IsMemberAsync(group.Id, ctx.User!.Id, ct);var members = await ctx.Groups.GetMembersAsync(group.Id, ct);| Метод | Описание |
|---|---|
FindByNameAsync / SearchAsync / GetAsync | Найти группу и узнать GroupType |
IsMemberAsync(groupId, userId) | Состоит ли пользователь в группе |
GetMembersAsync(groupId) | Список участников |
В Event Receiver / Timer Job нет Groups API — резолв группы через REST GET /groups?q= или заранее известный id из config. Timer Job: api.Users.ListActiveDirectoryUsersAsync для пользователей AD.
5. Группы из AD (domain_linked)
Заголовок раздела «5. Группы из AD (domain_linked)»Чем отличаются
Заголовок раздела «Чем отличаются»group_type | Откуда | Состав |
|---|---|---|
portal | Создана вручную в Portal | Меняете через UI / POST …/members |
domain_linked | Импортирована ad-sync из AD/LDAP | Только чтение в Portal; состав ведёт синхронизация |
Обе — полноценные группы ACL: их можно указывать в GrantToGroupAsync / назначениях прав так же, как обычные.
Как понять, что группа из AD
Заголовок раздела «Как понять, что группа из AD»var g = await ctx.Groups.FindByNameAsync("Domain Users IT", ct) ?? await ctx.Groups.GetAsync(knownId, ct);
if (g?.IsFromDirectory == true) // GroupType == "domain_linked"{ // состав меняет только ad-sync}Либо REST (admin): GET /api/v1/groups/{id} → поле group_type.
Проверить, что пользователь в AD-группе
Заголовок раздела «Проверить, что пользователь в AD-группе»Членство проверяется так же, как для любой группы:
// WebPart: groupId — UUID domain_linked группы из ad-syncbool inAdGroup = ctx.User!.GroupIds.Contains(adGroupId);// илиbool inAdGroup = await ctx.Groups.IsMemberAsync(adGroupId, ctx.User.Id, ct);Платформа при входе/синхронизации поддерживает состав group_members; GroupIds и IsMemberAsync уже учитывают AD-группы.
Нельзя вручную добавить/убрать участника из domain_linked через API — вернётся ошибка валидации («состав управляется ad-sync»).
Пользователь из домена
Заголовок раздела «Пользователь из домена»У участника в ответе GET /groups/{id} бывает auth_source: "domain". Карточка: ctx.Users.GetProfileAsync / REST /users/{id}/profile. Это про учётку, не про тип группы.
6. Типичные сценарии
Заголовок раздела «6. Типичные сценарии»Скрыть действие в WebPart
Заголовок раздела «Скрыть действие в WebPart»var list = await ctx.Permissions.ForListAsync(listId, ct);var html = list.CanAdd ? """<button type="button" data-wp-action="create">Создать</button>""" : "";Доступ только группе IT (в т.ч. из AD)
Заголовок раздела «Доступ только группе IT (в т.ч. из AD)»var it = await ctx.Groups.FindByNameAsync("Сотрудники IT", ct);if ((it is null || !ctx.User!.GroupIds.Contains(it.Id)) && !ctx.User!.IsPortalAdmin) return new WebPartResult("", "Доступ только для IT");После создания элемента выдать автору edit
Заголовок раздела «После создания элемента выдать автору edit»var created = await ctx.Lists.NewListItemAsync(listId, ct);created["title"] = title;await created.CreateAsync(ct);
// После CreateAsync: Id — номер элемента, ItemRef — строковый refawait ctx.Permissions.GrantToUserAsync( PermissionResource.ListItem, created.ItemRef, // или created.Id.ToString() ctx.User!.Id, PermissionLevel.Edit, listId: listId, ct: ct);Для числового id передавайте
"12"(илиItemRef) иlistId:; для UUID элемента — строку guid (можно безlistId).
Timer Job: массово выдать группе view на список
Заголовок раздела «Timer Job: массово выдать группе view на список»await api.Permissions.GrantToGroupAsync( PermissionResource.List, listId.ToString(), readersGroupId, PermissionLevel.View, ct: ct);7. Частые ошибки
Заголовок раздела «7. Частые ошибки»| Симптом | Причина | Что сделать |
|---|---|---|
Forbidden на Grant для списка | Не portal admin | Выполнять от admin / Timer Job / повысить роль |
Для числового resourceId укажите listId | Элемент по номеру без контекста | Передать listId: / libraryId: |
CanView == false у admin | Не admin в контексте | Проверить ctx.User.IsPortalAdmin |
| Не вижу AD-группу в UI состава | domain_linked | Состав только через ad-sync |
IsMemberAsync = false после добавления в AD | Ещё не синхронизировали | Дождаться ad-sync / запустить службу |
8. Куда смотреть дальше
Заголовок раздела «8. Куда смотреть дальше»| Тема | Документ |
|---|---|
| Полный REST permissions/groups | API: права и группы |
SDK WebPart (Permissions, Groups) | WebPart SDK |
| UI для пользователей | Права доступа |
| Группы в админке | Группы доступа |
| Права на узел / список (гайды) | Узлы, Списки |
PortalListItem | Кастомная форма |