Перейти к содержимому

Права доступа из кода

Как из расширения (WebPart / Event Receiver / Timer Job) проверить права пользователя, назначить ACL и узнать, состоит ли пользователь в группе портала или в группе из AD.

Полный REST-справочник: API: права и группы.
Для конечных пользователей (UI): Права доступа.


  1. Понять, откуда взять groupId / listId / nodeId (Property Pane, имя, slug, URL).
  2. Проверить эффективные права на узел / список / элемент / файл (CanViewCanDelete).
  3. Выдать или отозвать права пользователю или группе.
  4. Проверить членство в группе и отличить группу из AD (domain_linked).

УровеньAPIЧто можно
ПросмотрviewЧитать ресурс
Добавлениеadd+ создавать
Редактированиеedit+ изменять
Полный доступdelete+ удалять

Каждый следующий уровень включает предыдущие. В C#: enum PermissionLevel (View / Add / Edit / Delete).

РесурсPermissionResource / APIКак указать id
УзелNode / nodeUUID
СписокList / listUUID
Элемент спискаListItem / list_itemUUID или номер + listId
БиблиотекаLibrary / libraryUUID
Файл / папкаFile / fileUUID или номер + libraryId
СтраницаPage / pageUUID
ВложениеAttachment / attachmentUUID

Эффективные права считаются с наследованием (элемент → список → узел → …) и с учётом членства в группах. Администратор портала (IsPortalAdmin) всегда имеет полный доступ.

РасширениеКто проверяется / от чьего имени ACL
WebPartПользователь страницы (ctx.User)
Event ReceiverПользователь события (context.Userapi.Permissions)
Timer JobСистемная учётка portal-system (portal admin)

UUID не нужно копировать из админки вручную в каждый пример. В WebPart SDK есть резолв по имени, slug и URL/path.

Администратор выбирает список/узел/группу в настройках WebPart → в коде читаете UUID:

var listId = ctx.Properties.GetGuid("listId"); // listPicker
var nodeId = ctx.Properties.GetGuid("sourceNodeId"); // nodePicker
var 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;
var portal = await ctx.Nodes.FindBySlugAsync("portal", parentId: null, ct);
var hr = await ctx.Nodes.FindBySlugAsync("hr", parentId: portal!.Id, ct);
// Канонический path или URL вида https://portal.example/portal/hr/lists/tasks
var 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);
}

После установки UUID лежат в resourceMap / refs provisioner (lists.tasks, groups.itStaff). Их можно прокинуть в Property Pane по умолчанию или читать из настроек списка модуля.

НуженМетод
Группа по имени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
Узел по slugctx.Nodes.FindBySlugAsync("hr", parentId)
Всё из URL/pathctx.Routes.ResolvePathAsync("/portal/…")
Список из настроек WebPartctx.Properties.GetGuid("listId")
Текущий узел страницыctx.NodeId

Подключите using Portal.Contracts.Permissions;.

// Текущий узел страницы
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:

СвойствоТипСмысл
LevelPermissionLevel?Эффективный уровень (null = нет доступа)
CanView / CanAdd / CanEdit / CanDeleteboolФлаги по иерархии
Has(required)boolДостаточно ли уровня required

Тот же API на api.Permissions (без ForCurrentNodeAsync):

var item = await api.Permissions.ForListItemAsync(listId, itemRef, ct);
if (!item.CanEdit) return new { skipped = true, reason = "no edit" };
МетодЧто проверяет
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 для нужного уровня
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 }

РесурсКто может 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 платформа кладёт 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;
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.


group_typeОткудаСостав
portalСоздана вручную в PortalМеняете через UI / POST …/members
domain_linkedИмпортирована ad-sync из AD/LDAPТолько чтение в Portal; состав ведёт синхронизация

Обе — полноценные группы ACL: их можно указывать в GrantToGroupAsync / назначениях прав так же, как обычные.

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.

Членство проверяется так же, как для любой группы:

// WebPart: groupId — UUID domain_linked группы из ad-sync
bool 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. Это про учётку, не про тип группы.


var list = await ctx.Permissions.ForListAsync(listId, ct);
var html = list.CanAdd
? """<button type="button" data-wp-action="create">Создать</button>"""
: "";
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");
var created = await ctx.Lists.NewListItemAsync(listId, ct);
created["title"] = title;
await created.CreateAsync(ct);
// После CreateAsync: Id — номер элемента, ItemRef — строковый ref
await 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).

await api.Permissions.GrantToGroupAsync(
PermissionResource.List,
listId.ToString(),
readersGroupId,
PermissionLevel.View,
ct: ct);

СимптомПричинаЧто сделать
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 / запустить службу

ТемаДокумент
Полный REST permissions/groupsAPI: права и группы
SDK WebPart (Permissions, Groups)WebPart SDK
UI для пользователейПрава доступа
Группы в админкеГруппы доступа
Права на узел / список (гайды)Узлы, Списки
PortalListItemКастомная форма