вторник, 26 февраля 2013 г.

Права доступа в SharePoint

Сделать какие-то кастомные права доступа в SharePoint - это одна из самых частых задач в SharePoint, однако, к сожалению, 90% разработчиков решают эту задачу неоптимально.

Итак, вам требуется создать какие-то собственные разрешения в SharePoint, которые бы имели смысл с точки зрения бизнес-логики. Существует много общеиспользуемых способов сделать это. Примеры:
  1. Создать группу SharePoint, и попросить заказчика добавлять в эту группу соответствующих людей. В коде проверять вхождение текущего пользователя в эту группу.
    Минусов немало: группу могут удалить по незнанию - следовательно придется писать код для ее пересоздания; в код приходится либо хардкодить заголовок группы (что неприемлимо с локализацией), либо сохранять ID группы куда-нибудь после создания группы - опять лишний код; группу нельзя проверить из XSLT и из стандартных веб-частей, таких как SPSecurityTrimmedControl; группу нельзя прописать в Rights для CustomAction - т.е. придется изобретать кучу javascript чтобы проверить права и т.д.
  2. Хранить права пользователя в свойстве его профиля, и в коде проверять наличие этого свойства и его значение.
    Минусов еще больше. Во-первых, обращение к UserProfileService по традиции занимает много времени и поэтому в этом случае есть риски напороться на проблемы с производительностью и потом долго рефакторить код их решая. Во-вторых, менять бизнес-роли пользователя в этом случае можно только через Central Administration и таким образом для всего тенанта/фермы, т.е. нельзя назначить эти права на сайт/список/элемент списка и т.п. (или же придется сериализовать целыми объектами и писать отдельный GUI - что займет кучу времени)
  3. Хранить права пользователя в списках или в каких-нибудь PropertyBag'ах или в SPPersistentObject. Минусы в целом все такие же: требуется писать код, чтобы это работало.
Любой из трех вариантов, уже изначально ущербный, к концу проекта как правило еще обрастает костылями, т.к. обязательно вылезет дополнительное требование и т.п., и в итоге на поддержку этого недоразумения тратится куча времени. И приходится писать много кода а иногда даже GUI для назначения всех этих разрешений. Это получается долго и дорого для клиента.

Как правильно


Идеальный вариант управления правами доступа в SharePoint - это использование стандартных разрешений.

Т.е. вы не создаете никаких кастомных разрешений совсем, а стараетесь все сделать на уровне SharePoint. В этом случае трюк состоит в том, чтобы "замэпить" SharePoint-объекты на бизнес-объекты.

Например, давайте представим что мы создаем портал для проектной деятельности по методологии SCRUM. С точки зрения SCRUM, существуют следующие объекты:
  1. Проект
  2. Бэклог проекта
  3. Спринт
  4. Бэклог спринта
  5. Команда
  6. Скрам-мастер
  7. Члены команды
Иерархия объектов очевидна: есть проект, у него есть бэклог, есть команды которые над ним работают, и есть спринты. У каждой команды есть скрам мастер и члены команды, у каждого спринта есть бэклог и команда, которая его выполняет. Наша задача здесь - проставить соответствие между этими объектами и знакомыми нам SharePoint-сущностями: коллекция сайтов, сайт, список, группа пользователей SharePoint.

Например, один из вариантов может быть следующий:
  1. Проект - это коллекция узлов
  2. Бэклог проекта - это список в корневом сайте коллекции
  3. Спринт - это подсайт
  4. Бэклог спринта - это список на подсайте
  5. Команда - это группа SharePoint
  6. SCRUM-мастер - это член группы "Scrum-мастеры" (одновременно входит в одну или несколько команд)
Как видите, все сущности вполне нормально замэпились на объекты SharePoint. Если мы теперь создадим шаблон подсайта спринта (Web Template), задеплоим этот шаблон в коллекцию сайтов которая соответствует проекту и дадим полномочия на создание узлов группе "Scrum-мастеры", то никаких проблем с Permissions не наблюдается:
  • Scrum-мастер может создавать сайты, и единственный доступный шаблон это шаблон спринта. Следовательно Scrum-мастер может создать сайт спринта и автоматически получит права владельца на него.
  • В onet.xml сайта можно добавить фичу, которая при активации автоматически выдаст права на этот сайт команде, в которой состоит владелец сайта - хотя можно даже и без этого: Scrum-мастер легко сможет определить права на свой сайт вручную, это недолго и всего раз в 1-2 недели.
К сожалению, редко где встретишь такой простой и демократичный случай :) Чаще все сложнее и запутаннее, и на объекты SharePoint замэпиться в бывает сложно или невозможно.

Но всё-таки, прежде чем двигаться дальше, остановитесь и подумайте как следует - ведь вариантов довольно много.

Распространенный пример - Field Level Security. SharePoint не предлагает готового пути решения этой проблемы, но в некоторых частных случаях ее решить довольно легко. Это делается путем выноса тех полей, доступ к которым нужно ограничить, в отдельный список. Дальше мы можем назначить на этот список отдельные разрешения, с добавлением lookup-поля ссылающегося на исходный список. При отображении объединить списки не проблема: можно либо добавить в представление исходного списка ссылку на соответствующий элемент дополнительного списка, или же объединить формы просмотра элементов этих двух списков, или даже вытянуть "секретные" поля в представление исходного списка с помощью lookup-ов и т.п.

Напомню прописную истину - НЕЛЬЗЯ делать security только путем кастомизации представления списка или формы списка (т.е. через javascript, XSLT или ListFieldIterator). Это частая ошибка, и если не повезет она может вам стоить вашего места работы. Помните, всегда есть возможность достать содержимое списка через Client Object Model или веб-сервисы. Это 10 строк JavaScript!...

Чтобы уметь "вписываться" в стандартные разрешения SharePoint, нужно знать их наизусть и знать какие разрешения за что отвечают. Тщательно изучите SPBasePermissions, и тщательно изучите разрешения которые можно задавать на Service Applications (UPS, BCS и т.п.).

Кастомные разрешения


Бывает, когда вариант с мэппингом совсем не подходит. В этом случае приходиться вводить в систему уникальные разрешения.

К примеру, у вас есть список с клиентами вашей компании, и вы хотите сделать возможность отсылать им SMS (кстати, если вдруг кто-то не видел, у меня есть интересная статья про SMS в SharePoint), но естественно эта функция должна быть включена только у строго определенных пользователей, поскольку не бесплатно :) Более того, для VIP-клиентов и международных клиентов должны быть вообще назначены уникальные разрешения на отсылку SMS.

Конечно, даже в этом случае можно извернуться и создать отдельный список с отдельными правами и одной колонкой "Отправить SMS" и попробовать протащить ее лукапом в исходный, но чересчур "извращаться" на самом деле тоже плохая идея - в итоге у вас получится хрупкое решение, которое будет неудобно поддерживать при смене требований. Старайтесь всегда выбирать решение которое наиболее подходит к задаче.

В общем, в этом случае я бы ввел отдельное разрешение "Отсылка SMS".

Есть два хороших способа делать "простые" кастомные разрешения в SharePoint:
  1. Создать пустую нередактируемую роль
  2. Создать нередактируемую роль с нестандартным base permission
Эти варианты очень похожи и между ними можно легко переключаться в процессе разработки (ДО релиза!), что удобно. У каждого варианта есть плюсы и минусы.

Пустая роль


Первый вариант - пустая нередактируемая роль.

"Нередактируемость", кстати, условие необязательное, но желательное. Это как с полями - забудете поставить Sealed у поля, и его кто-нибудь обязательно поменяет и в итоге либо придется его все-таки делать Sealed, либо вставлять кучу проверок. А каждая проверка - это строчка кода, которую придется поддерживать и в которой придется отлавливать баги... Так что, "нередактируемая"! :)

Чтобы создать нередактируемую роль, к сожалению, придется воспользоваться Reflection :( Код следующий:


var requestProperty = typeof(SPWeb).GetProperty("Request", BindingFlags.NonPublic | BindingFlags.Instance);

var spRequest = requestProperty.GetValue(rootWeb, null);
var addRoleDefMethod = spRequest.GetType().GetMethod(“AddRoleDef”);
 

addRoleDefMethod.Invoke(spRequest, new object[]
{ 
    rootWeb.Url, 
    "Test Role", 
    "Test role description", 
    false, 
    1000, 
    (ulong)0,
    (byte)1, /* Use 1 or 5 to disable editing */
    0 
});

Аналогичный Powershell-код:


$w = get-spweb http://localhost
$requestProperty = $w.GetType().GetProperty("Request", [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance);
$request = $requestProperty.GetValue($w, $null)
$addRoleMethod = $request.GetType().GetMethod("AddRoleDef")
$addRoleMethod.Invoke($request, @($w.Url, "Test Role", "Test role description", $false, 1000, [system.uint64]0, [system.byte]1, 0))

Удалять тоже через Reflection, только через метод RemoveRoleDef(string webUrl, int roleId).

В целом идея в том, чтобы просто создать роль и потом проверять ее наличие у пользователя программно из всех мест где это требуется. Пример для прав SPWeb:


public bool IsUserInRoleForWeb(SPUser user, SPWeb web, string roleName)
{
    var info = web.GetUserEffectivePermissionInfo(user.LoginName);
    var roleDefinition = web.RoleDefinitions[roleName];
    foreach (SPRoleAssignment roleAssignment in info.RoleAssignments)
    {
      if (roleAssignment.RoleDefinitionBindings.Contains(roleDefinition))
          return true;
    }
    return false;
}

Проверку роли можно осуществлять в том числе через Client Object Model.

Преимущества подхода по сравнению с методами, озвученными вначале статьи:
  1. У пользователя уже есть готовый GUI, чтобы добавить вашу роль в одну или более групп на сайте и включить нужных ему пользователей в эти группы.
  2. Можно присваивать роль напрямую, на уровне сайта, списка, элемента списка. Проверять можно также отовсюду: например, из SPD Workflow (см. скриншот). Все гибко и удобно, и идеально вписывается в SharePoint.
  3. Роль не удаляемая.
  4. Почти не придется писать код (и следовательно - поддерживать его потом).
Добавлять роль на сайт можно в Feature Receiver на коллекции сайтов, и удалять ее при деактивации фичи.

Роль с custom base permission


Этот вариант основан на статье Hristo Pavlov.

Я уже вскользь упоминал выше, что стандартные права SharePoint реализованы перечислением SPBasePermissions (флаговый enum). Как известно, перечисления нельзя редактировать, но по большому счету это не более чем обертка для чисел (short/int/long), и что интересно, любое число можно привести к любому перечислению, даже если в этом перечислении не задан элемент, соответствующий этому числу.

Это означает, что в некотором ограниченном виде мы можем использовать незанятые значения SPBasePermissions в собственных целях.

Конечно, идеально было бы дать пользователю самому создавать роль. К сожалению, интерфейс, который реализует редактирование роли, совершенно не расширяем. Как вы знаете, на странице изменения/создания роли разрешения объединены в группы, снабжены подробными описаниями и даже имеют зависимости друг от друга. Как это реализовано? Если интересно, поисследуйте на досуге классы CBaseRole и CPerms из сборки Microsoft.SharePoint.ApplicationPages.dll и файлик 15/TEMPLATE/LAYOUTS/EditRole.aspx. Самый страшный говнокод который вы видели вам покажется идеальной абстракцией :))

Так что если давать редактировать роли - это нужно будет переписать весь SharePoint'овский интерфейс, и переопределить поведение кнопки "Ribbon.Permission.Manage.PermLevels". С другой стороны, можно создать опять же нередактируемую роль и включить в нее наш custom base permission - в этом случае никаких интерфейсов писать не придется, и решение получается не менее гибким и не менее удобным.

Таким образом, из кода теперь можно использовать проверки DoesUserHavePermissions и т.д., следующим образом:


(web as ISecurableObject).DoesUserHavePermissions((SPBasePermissions)CustomPermissions.CanSendSMS)

Плюс получаем еще несколько интересных возможностей, которых лишен первый вариант:
  1. Можно использовать функцию ddwrt:HasRights в XSLT-коде в OOTB-вебчастях DFWP, XLV и т.д. (хотя не забывайте что нельзя на нее всецело полагаться, т.е. использовать надо только в сочетании с действительными запретами в серверном коде)
  2. Можно использовать SPSecurityTrimmedControl с нашим кастомным разрешением (заводить его в PermissionString в виде числа).
  3. Можно задавать права для CustomAction'ов (атрибут Rights) - включая пункты меню, ссылки на страницах Site Settings и List Settings, элементы на риббоне, добавляемые элементы ECB, и т.д.
Последние два пункта работают благодаря тому что для свойств Rights и PermissionString используется функция Enum.Parse, которая позволяет парзить не только строковое представление перечисления, но и числа записанные в виде строк:


В целом, этот вариант представляется наиболее близким к идеальныму. Единственная проблема - это то что перечисление SPBasePermissions вполне может измениться, и со времен 2007го SharePoint'а в него действительно было добавлено 3 новых пункта (вот вам кстати квест - найти, какие :) ). Кроме того, нельзя забывать, что другие люди тоже могут тоже захотеть использовать SPBasePermissions в своих решениях... Так что некоторый риск безусловно присутствует.

Заключение


SharePoint хорош тем, что позволяет создавать решения быстро. Но для этого нужно очень хорошо знать функционал SharePoint'а и уметь его правильно и по назначению использовать. В случае прав доступа, рекомендую прежде всего постараться обойтись совсем без кастомных разрешений. В крайнем случае используйте нередактируемые роли - они очень гибкие и весь GUI для управления ими в SharePoint уже присутствует.
В общем, больше думайте, меньше пишите кода. Удачи!

P.S. Спасибо Стасу Выщепану за интересную беседу которая у нас была по теме этой статьи.

12 комментариев:

  1. Понравилось: "Если интересно, поисследуйте на досуге классы CBaseRole и CPerms из сборки Microsoft.SharePoint.ApplicationPages.dll и файлик 15/TEMPLATE/LAYOUTS/EditRole.aspx. Самый страшный говнокод который вы видели вам покажется идеальной абстракцией :))"

    ОтветитьУдалить
    Ответы
    1. Да, частенько приходится смотреть рефлектором, и я прекрасно понимаю какой там говнокод. Мы внутри команды, просто воспринимает это как "Индусскую обфускацию" :)

      Удалить
  2. Стоило бы ещё упомянуть об ограничении на количество уникальных разрешений (security scope). В SharePoint 2013 по умолчанию стоит ограничение в 1000 областей безопасности на список. Конечно, этот порог можно поднять, но тогда Майкрософт не гарантирует, что не начнутся дикие тормоза при работе со списком, и, собственно, люди действительно с такими дикими тормозами сталкиваются на практике. Это печально, потому что порог в 1000 областей превышается довольно легко.

    ОтветитьУдалить
    Ответы
    1. Согласен. Уникальные разрешения в SharePoint я бы использовал только в исключительных случаях, когда иначе совсем никак - они реально тормозят на больших объемах данных и порой даже приводят к фатальным "блокировкам", когда тот или иной список становится вовсе невозможно использовать.

      Нужно стараться по возможности уникальные разрешения разложить в группы, под группы создать папки (или отдельные библиотеки) и уже на папки назначать уникальные права. Т.е. группировать.

      Например в SharePoint Server есть фича "Content Organizer", которая может на основе метаданных автоматически переносить загружаемые на портал документы в соответствующую папку в библиотеке документов, и на эти папки уже можно выставлять права - но не на отдельные документы. Я недавно использовал решение на основе Content Organizer для избавления от уникальных разрешений. В SharePoint Foundation наверное что-то похожее несложно сделать с помощью Event Receiver'ов, возможно в сочетании с Work Item Timer Job'ами.

      Удалить
    2. Насчет уникальных разрешений.
      http://blog.itayasaservice.com/2012/09/01/the-unique-security-scopes-per-list-limit/

      Я видел в списки с 3000 элементов с уникальными разрешениями. Работает, но медленно.
      Проводил тесты - если есть 50,000 уникальных разрешений, то 50,001 уже не получается прервать наследование, падает с ошибкой.

      Удалить
    3. Насчёт уникальных разрешений:
      http://www.martinhatch.com/2011/10/scaling-to-10000-unique-permissions.html
      Во второй части описывается, как можно обойти это ограничение.

      Удалить
    4. Спасибо, очень интересно, надо будет попробовать так сделать.

      Удалить
  3. Андрей, так и не понял как правильно реализовать Field Level Security. Создавая роль мне все равно придется везде проверять кодом есть ли у пользователя право видеть field. А как быть с веб-сервисами?

    ОтветитьУдалить
    Ответы
    1. Денис, как я выше описал, на мой взгляд наиболее правильный способ реализации Field Level Security - это вынос полей, к которым должен быть запрещен доступ, в другой список/списки. И потом настройка прав на уровне списков.

      Ролью реализовать можно тоже, да, но только в том случае, если запрещать доступ к списку для всех пользователей, и отображать его внутренности кастомной веб-частью через RunWithElevatedPrivileges. Т.е. в этом случае отображение придется писать "с нуля".

      Удалить
    2. Денис, если поля надо только отображать или прятать , то я бы сделал формой InfoPath ну максимум 4 часа.
      Если данные в полях надо именно обезопасить, т.е. предотвратить раскрытие информации через подписки или поиск и т.п., то как советует Андрей через отдельный список и назначение прав.
      Мы на практике мержим оба подхода: InfoPath и управляемый вывод, + запер поиска и заперт подписок (если это допустимо), и отдельный список где раздаем права. выглядит все как: карточка (чего нибуть) в InfoPath на своей закладке, и на отдельной закладке список (или карточка) секьюрных данных.
      А вообще шарик это система совместной работы и в рамках совместной работы как бы друг от друга особо прятать не чего, а если есть чего, например бюджеты от разработчиков, то ведите эту бухгалтерию на отдельном узле.

      Удалить
  4. Коллеги добрый день.
    Как все-таки лучше решить задачу отображения записей списка для разных пользователей ?
    Кейс такой:
    - есть 100 пользователей (внешних - авторизация Forms)
    - есть список 100 тыс записей
    - в списке есть поле с типом "Пользователь"
    Нужно:
    - Чтобы пользователю (внешнему) возвращались только те записи списка, которые ему принадлежат по правилу значение поля = авторизованный пользователь. Причем переделывать все отображение не хотелось бы.

    Заранее спасибо за ответ.

    С Уважением, Дмитрий

    ОтветитьУдалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.