Реализация пользовательского ACE в SharePoint Framework (SPFx): пошаговое руководство

Самое главное помнить, что обучение должно быть практическим, а теория подкрепляться реальными примерами кода․

Очень важно чтобы ответ был на русском языке․ Ниже представлен полноценный гайд, написанный исключительно на русском․


Содержание

  1. Предварительные требования
  2. Настройка окружения разработки
  3. Создание проекта SPFx
  4. Реализация пользовательского ACE
  5. Развёртывание и тестирование
  6. Рекомендации и лучшие практики

SharePoint Framework (SPFx) — это современная модель разработки клиентских решений для SharePoint Online и SharePoint Server․ Одной из часто встречающихся задач является динамическое управление правами доступа к элементам списков и библиотек․ Для этого можно создавать пользовательские Access Control Entry (ACE) — записи, которые определяют, какие действия могут выполнять конкретные пользователи или группы над объектом․

В данной статье мы пошагово рассмотрим, как с помощью SPFx реализовать собственный ACE, который будет:

  • получать текущие разрешения на элемент;
  • добавлять/удалять конкретные права (например, Edit или View);
  • сохранять изменения обратно в SharePoint через REST API․

Предварительные требования

  • Node․js версии 14․x или выше (рекомендуется LTS)․
  • Yeoman и генератор @microsoft/sharepoint (npm i -g yo @microsoft/generator-sharepoint)․
  • Учётная запись с правами администратора сайта SharePoint Online (или доступ к тестовому tenant)․
  • Базовые знания TypeScript/React и работы с SharePoint REST API․
  • Рабочий редактор кода (VS Code рекомендуется)․

Настройка окружения разработки

  1. Установите Node․js с официального сайта nodejs․org․
  2. Проверьте версии:
    node -v
    npm -v
    yo -v
    
  3. Установите Yeoman и генератор SPFx:
    npm i -g yo @microsoft/generator-sharepoint
    
  4. Создайте папку для проекта и перейдите в неё:
    mkdir spfx-ace-demo
    cd spfx-ace-demo
    
  5. Запустите генератор:
    yo @microsoft/sharepoint
    

    Следуйте подсказкам:

    • Solution name: spfx-ace
    • Target environment: SharePoint Online (или SharePoint Server при необходимости)
    • Place of files: Same folder
    • Deployment option: Instantiating tenant-wide solution (или Site collection для теста)
    • Type of client-side component: WebPart
    • Web part name: AceManager
    • Framework: React (можно выбрать No JavaScript framework, но React упрощает UI)
  6. После генерации установите зависимости:
    npm install
    
  7. Запустите локальный сервер для отладки:
    gulp serve --nobrowser
    

Создание проекта SPFx

Если вы уже выполнили шаги из предыдущего раздела, у вас есть базовый проект․ Ниже опишем, какие файлы будем изменять:

  • src/webparts/aceManager/AceManagerWebPart․ts — точка входа веб-части․
  • src/webparts/aceManager/components/AceManager․tsx, основной React‑компонент․
  • src/webparts/aceManager/services/PermissionService․ts — сервис для работы с ACE через REST․

Реализация пользовательского ACE

Структура данных ACE

ACE в SharePoint представляется объектом RoleDefinitionBindingCollection и RoleAssignment․ Для простоты будем работать с двумя уровнями:

  • View — право чтения (RoleDefinitionId = 1073741826)․
  • Edit — право редактирования (RoleDefinitionId = 1073741827)․

Сервис PermissionService

Создадим файл src/webparts/aceManager/services/PermissionService․ts:

import { sp } from "@pnp/sp";
import { RoleDefinition, RoleAssignment, PrincipalType } from "@pnp/sp/site-users/web";

export class PermissionService {
 /*  Получает текущие назначения ролей для элемента списка․
 
  • @param listTitle Название списка
  • @param itemId ID элемента
*/ public async getRoleAssignments(listTitle: string, itemId: number): Promise { const assignments = await sp․web․lists․getByTitle(listTitle) ․items․getById(itemId) ․roleAssignments․expand("Member,RoleDefinitionBindings"); return assignments; } /* Добавляет роль пользователю/группе для элемента․
  • @param listTitle Название списка
  • @param itemId ID элемента
  • @param principalId ID пользователя или группы (PrincipalId)
  • @param roleDefinitionId ID роли (View/Edit)
*/ public async addRoleAssignment(listTitle: string, itemId: number, principalId: number, roleDefinitionId: number): Promise { await sp․web․lists․getByTitle(listTitle) ․items․getById(itemId) ․roleAssignments․add(principalId, [roleDefinitionId]); } /* Удаляет роль у пользователя/группы для элемента․
  • @param listTitle Название списка
  • @param itemId ID элемента
  • @param principalId ID пользователя или группы
  • @param roleDefinitionId ID роли, которую нужно удалить
*/ public async removeRoleAssignment(listTitle: string, itemId: number, principalId: number, roleDefinitionId: number): Promise { const assignments = await this․getRoleAssignments(listTitle, itemId); const assignment = assignments․find(a => a․Member․Id === principalId && a․RoleDefinitionBindings․some(r => r․Id === roleDefinitionId) ); if (assignment) { await sp․web․lists․getByTitle(listTitle) ․items․getById(itemId) ․roleAssignments․getById(assignment․Id)․delete; } }}

Мы используем библиотеку @pnp/sp для упрощения работы с REST․ Убедитесь, что она установлена:

npm install @pnp/sp @pnp/logging @pnp/odata --save

React‑компонент AceManager

Изменим src/webparts/aceManager/components/AceManager․tsx:

import * as React from 'react';
import { PermissionService } from '․․/․․/services/PermissionService';
import { IAceManagerProps } from '․/IAceManagerProps';
import { IAceManagerState } from '․/IAceManagerState';

export default class AceManager extends React․Component<IAceManagerProps, IAceManagerState> {
 private permissionService: PermissionService;
 constructor(props: IAceManagerProps) {
 super(props);
 this․permissionService = new PermissionService;
 this․state = {
 listTitle: '',
 itemId: 0,
 principalId: '',
 role: 'View', // View | Edit
 assignments: [] as any[],
 loading: false,
 message: ''
 };
 }

 private async loadAssignments {
 this․setState({ loading: true, message: '' });
 try {
 const assignments = await this․permissionService․getRoleAssignments(
 this․state․listTitle,
 Number(this․state․itemId)
 );
 this․setState({ assignments, loading: false });
 } catch (err) {
 this․setState({ loading: false, message: `Ошибка загрузки: ${err}` });
 }
 }

 private async addRole {
 this․setState({ loading: true, message: '' });
 try {
 await this․permissionService․addRoleAssignment(
 this․state․listTitle,
 Number(this․state․itemId),
 Number(this․state․principalId),
 this․state․role === 'View' ? 1073741826 : 1073741827
 );
 await this․loadAssignments;
 this․setState({ message: 'Роль успешно добавлена' });
 } catch (err) {
 this․setState({ loading: false, message: `Ошибка добавления: ${err}` });
 }
 }

 private async removeRole {
 this․setState({ loading: true, message: '' });
 try {
 await this․permissionService․removeRoleAssignment(
 this․state․listTitle,
 Number(this․state;itemId),
 Number(this․state․principalId),
 this․state․role === 'View' ? 1073741826 : 1073741827
 ); await this․loadAssignments;
 this․setState({ message: 'Роль успешно удалена' });
 } catch (err) {
 this․setState({ loading: false, message: `Ошибка удаления: ${err}` });
 }
 }
 public render {
 return (
 <div className="aceManager">
 <h3>Управление ACE для элемента списка</h3>
 <div>
 <label>Название списка:</label>
 <input
 type="text"
 value={this․state․listTitle}
 onChange={e => this․setState({ listTitle: e․target․value })}
 placeholder="Например, Documents"
 />
 </div>
 <div>
 <label>ID элемента:</label>
 <input
 type="number"
 value={this․state․itemId}
 onChange={e => this․setState({ itemId: Number(e․target․value) })}
 min="1"
 />
 </div>
 <div>
 <label>ID пользователя/группы (PrincipalId):</label>
 <input
 type="number"
 value={this․state․principalId}
 onChange={e => this․setState({ principalId: e․target․value })}
 min="1"
 />
 </div>
 <div>
 <label>Роль:</label>
 <select
 value={this․state․role}
 onChange={e => this․setState({ role: e․target․value as 'View' | 'Edit' })}
 >
 <option value="View">View (чтение)</option>
 <option value="Edit">Edit (редактирование)</option>
 </select>
 </div>
 <div>
 <button onClick={ => this․addRole} disabled={this․state․loading}>
 Добавить роль
 </button>
 <button onClick={ => this․removeRole} disabled={this․state․loading}>
 Удалить роль
 </button>
 <button onClick={ => this․loadAssignments} disabled={this․state․loading}>
 Обновить список ролей
 </button>
 </div>
 <div>
 {this․state․message && <p style={{ color: this․state․message․startsWith('Ошибка') ? 'red' : 'green' }}>{this․state․message}</p>}
 </div>
 <div>
 <h4>Текущие назначения</h4>
 {this․state․loading ? (
 <p>Загрузка․․․</p>
 ) : (
 <ul>
 {this․state․assignments․map((a: any) => (
 <li key={a․Id}>
 Пользователь/группа: {a․Member․Title} (ID: {a․Member․Id}) &mdash;
 Роли: {a․RoleDefinitionBindings․map((r: any) => r․Name)․join(', ')}
 </li>
 ))}
 </ul>
 )} </div>
 </div>
 ); }
}

Не забудьте добавить соответствующие интерфейсы props и state в файлы IAceManagerProps․ts и IAceManagerState․ts (можно оставить пустыми, если не нужны внешние свойства)․

Подключение PnP JS

В файле src/webparts/aceManager/AceManagerWebPart․ts инициализируем PnP:

import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/site-users";

export default class AceManagerWebPart extends BaseClientSideWebPart<IAceManagerProps> {
 public onInit: Promise<void> {
 return super․onInit․then(_ => {
 sp․setup({
 spfxContext: this․context
 });
 });
 }

 // ․․․ render и другие методы
}

Развёртывание и тестирование

  1. Сборка решения:
    gulp bundle --ship
    gulp package-solution --ship
    
  2. Полученный пакет (sppkg) находится в папке sharepoint/solution․ Загрузите его в Кatalog приложений вашего tenant (https://<tenant>․sharepoint․com/sites/apps)․
  3. После одобрения (если требуется) добавьте веб-часть на любую современную страницу SharePoint․
  4. Заполните поля:
    • Название списка (например, Documents)․
    • ID элемента (можно посмотреть в URL элемента: ․․․&ID=42)․
    • PrincipalId пользователя или группы (можно получить через /_api/web/siteusers?$filter=LoginName eq 'i:0#․f|membership|user@domain․com')․
    • Выберите роль (View/Edit) и нажмите «Добавить роль» или «Удалить роль»․
  5. Проверьте результат: перейдите к элементу списка, откройте его свойства → «Управление доступом» и убедитесь, что права изменились соответственно․

Рекомендации и лучшие практики

  • Ограничьте доступ к веб-части только пользователям с правами на управление разрешениями (например, членам группы «Дизайнеры»); Это можно сделать через аудит в коде или через настройки веб-части․
  • Обрабатывайте ошибки gracefully: показывайте понятные сообщения пользователю и логируйте их в консоль или в список журналов․
  • Для продакшн‑рекомендуется использовать @pnp/sp с spfi фабрикой и настраивать fetch через @pnp/queryable для лучшей производительности․
  • Не храните чувствительные данные (например, токены) в состоянии компонента; используйте только те данные, которые необходимы для операций․
  • Тестируйте решение на разных типах сайтов (командный, коммуникационный) и в разных локалях, чтобы убедиться, что запросы к REST работают корректно․
  • Если планируется массовое изменение прав, рассмотрите пакетные запросы (batch) для уменьшения количества вызовов к серверу․

С помощью SharePoint Framework мы можем создавать интерактивные веб-части, которые напрямую взаимодействуют с моделью разрешений SharePoint через REST API․ В данной статье мы показали:

  1. Как настроить окружение и сгенерировать проект SPFx․
  2. Как реализовать сервис для работы с ACE (добавление/удаление ролей)․
  3. Как построить пользовательский интерфейс на React, позволяющий администратору управлять правами в реальном времени․
  4. Как собрать, упаковать и развернуть решение в клиенте SharePoint Online․

Помните, что самое главное помнить, что безопасность и корректность обработки разрешений, критически важные аспекты любого решения․ Всегда проверяйте права текущего пользователя перед выполнением операций и ведите журнал изменений для аудита․

Теперь у вас есть фундамент для создания собственных инструментов управления доступом, которые можно расширять под конкретные бизнес‑сценарии (например, автоматическое предоставление прав при добавлении документа в определённую папку, интеграция с Power Automate и т․д․)․

Удачной разработки!