Библиотека служит обёрткой для методов запроса пользовательских прав у сервера авторизации.
- monq-core-authorization
- История изменений
- Установка
- Подключение
- Реализуемые методы расширения
- Subject()
- Userspace()
- Packets()
- IsSystemUser()
- IsUserspaceAdmin(long userspaceId)
- IsSuperUser(long userspaceId)
- HasGrant(long userspaceId, long workGroupId, string grantName)
- HasAnyGrant(long userspaceId, long workGroupId, IEnumerable<string> grantNames)
- HasAllGrants(long userspaceId, long workGroupId, IEnumerable<string> grantNames)
- GetWorkGroupsWithGrant(long userspaceId, string grantName)
- GetWorkGroupsWithAnyGrant(long userspaceId, IEnumerable<string> grantNames)
- GetWorkGroupsWithAllGrants(long userspaceId, IEnumerable<string> grantNames)
- WorkGroups(long userspaceId)
- Userspaces()
- Тестирование
- Произведено обновление до .NET Core 3.0
- Переломные изменения:
- Удалено свойство владельца пакета прав openWorkGroups. Значения идентификаторов рабочих групп, которым предоставляется доступ к пакету, больше не учитываются.
- Переломные изменения:
- Подключение библиотеки авторизации теперь не требует отдельного класса опций авторизации. Вместо этого используются стандартные поставщики конфигурации приложения; сервис пользовательских прав задаётся ключом "
BaseUri
". - Перечисленные ниже методы теперь требуют обязательного указания идентификатора пользовательского пространства:
- Удалён IsCloudAdmin() как избыточный и не отвечающий требованиям безопасности.
- Метод IsUserspaceAdmin() теперь возвращает
false
для системного пользователя. - Удалено AutoAssign-свойство псевдореализации прав
FakeGrantsImpl
. - RevertToDefaults() перенесён из
GrantsExtensions
вFakeGrantsImpl
, где имеет больше смысла и не требует добавления дополнительных пространств имён.
- Подключение библиотеки авторизации теперь не требует отдельного класса опций авторизации. Вместо этого используются стандартные поставщики конфигурации приложения; сервис пользовательских прав задаётся ключом "
- Улучшения:
- Методы HasGrant(), HasAnyGrant(), HasAllGrants() теперь безусловно возвращают
true
, если вызваны администратором пользовательского пространства (аналогично системному пользователю). - Добавлен метод IsSuperUser() для проверки пользователя на права системного пользователя или администратора данного пространства.
- Методы HasGrant(), HasAnyGrant(), HasAllGrants() теперь безусловно возвращают
- Переломные изменения:
- Перечисленные ниже методы теперь требуют обязательного указания идентификатора пользовательского пространства:
- При невозможности получить идентификатор пользовательского пространства в методе Userspace() теперь не возвращается значение по умолчанию (было
0
), а выбрасывается исключение типаUserspaceNotFoundException
.
Install-Package Monq.Core.Authorization
Для корректного подключения один из поставщиков конфигурации приложения должен содержать значение ключа "BaseUri
" с адресом сервиса пользовательских прав.
Для задания опций и подключения Middleware в методе конфигурации (Configure()) приложения следует указать:
public IConfiguration Configuration { get; set; }
public Startup(IConfiguration configuration)
=> Configuration = configuration;
public void Configure(IApplicationBuilder app)
{
...
app.UseMonqAuthorization(Configuration);
...
app.UseMvc();
}
Подключение авторизации следует производить перед app.UseMvc().
Все методы расширения определены в пространстве имён Microsoft.AspNetCore.Authorization
.
Для сценариев, в которых необходимо получить системный идентификатор пользователя запроса из ClaimsPrincipal
свойства User, используется метод расширения Subject().
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var subjectId = User.Subject(); // id пользователя запроса (например, 23).
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Возвращает значение типа 64-разрядное знаковое целое, long.
Особые случаи:
- для системного пользователя возвращает -1;
- в случае любой ошибки возвращает 0.
Для сценариев, в которых необходимо получить идентификатор пространства пользователя запроса из HTTP-заголовков свойства Request, используется метод расширения Userspace().
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var userspaceId = Request.Userspace(); // id пользовательского пространства (например, 1).
...
}
}
Свойство контроллера Request унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Возвращает значение типа 64-разрядное знаковое целое, long.
Особые случаи:
- в случае любой ошибки возвращает исключение типа
UserspaceNotFoundException
.
Для авторизации действий пользователя запроса из ClaimsPrincipal
свойства User в контроллерах используется метод расширения Packets(), который позволит получить пакеты прав пользователя запроса.
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var packets = User.Packets(); // перечисление пакетов прав пользователя.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Метод возвращает значение типа перечисление с моделью PacketViewModel
, определённой в соответствующем файле проекта (src\Monq.Core.Authorization\ViewModels\PacketViewModel.cs), со значимыми свойствами:
public class PacketViewModel
{
/// <summary>
/// Идентификатор пакета пользовательских прав.
/// </summary>
public long Id { get; set; }
/// <summary>
/// Имя пакета пользовательских прав.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Описание пакета пользовательских прав.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Является ли пакет доступным только для чтения.
/// </summary>
public bool IsReadOnly { get; set; }
/// <summary>
/// Коллекция прав доступа пакета.
/// </summary>
public IEnumerable<string> Grants { get; set; }
/// <summary>
/// Коллекция владельцев пакета.
/// </summary>
public IEnumerable<PacketOwnerViewModel> Owners { get; set; }
}
Где значимые:
- Name -- имя пакета прав. Например,
Администратор пространства
. - IsReadOnly -- флаг принадлежности пакета к системным. Например, true.
- Grants -- перечисление строковых трёхсоставных определений прав. Например,
{ "base-system.rsm.read", "cloud-management.grants-meta.read" }
. - Owners -- коллекция рабочих групп-владельцев и их пользователей пакета прав.
..., -- и, соответственно, PacketOwnerViewModel
, определённой в соответствующем файле проекта (src\Monq.Core.Authorization\ViewModels\PacketOwnerViewModel.cs), со значимыми свойствами:
public class PacketOwnerViewModel
{
/// <summary>
/// Идентификатор рабочей группы-владельца пакета.
/// </summary>
public long WorkGroupId { get; set; }
/// <summary>
/// Идентификатор пользовательского пространства.
/// </summary>
public long UserspaceId { get; set; }
/// <summary>
/// Коллекция идентификаторов пользователей пакета.
/// </summary>
public IEnumerable<long> Users { get; set; }
}
Где:
- WorkGroupId -- идентификатор рабочей группы в сервисе рабочих групп. Например,
23
. - UserspaceId -- идентификатор пользовательского пространства рабочей группы. Например,
1
. - Users -- коллекция идентификаторов пользователей рабочей группы, имеющих доступ до прав пакета. Например,
{ 1, 15, 41 }
.
..., -- которые позволяют вкупе исчерпывающе определить права пользователя в каждой рабочей группе.
Для простоты восприятия можно воспринимать пакеты прав как роли рабочих групп, владельцев пакетов -- как рабочие группы, в которых эти роли определены.
Для проверки, является ли пользователь запроса из ClaimsPrincipal
свойства User системным пользователем (т.е. другим сервисом, тестом и т.д.) используется метод расширения IsSystemUser().
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var hasSystemGrants = User.IsSystemUser(); // true чаще всего означает отсутствие дальнейших проверок.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Метод возвращает true
:
- если у пользователя в его Claim 'ах присутствует идентификатор системного пользователя;
Для проверки наличия прав администрирования данного облачного пространства у пользователя запроса из ClaimsPrincipal
свойства User используется метод расширения IsUserspaceAdmin(long userspaceId).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var canCreateWorkGroups = User.IsUserspaceAdmin(17);
// true, если пользователь администрирует данное пользовательское пространство.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
- userspaceId -- 64-разрядное знаковое целое, идентификатор пользовательского пространства, администрирование которого проверяется. Например,
17
.
Метод возвращает true
:
- если пользователь является администратором данного пользовательского пространства;
Для проверки наличия прав системного пользователя или администрирования данного облачного пространства у пользователя запроса из ClaimsPrincipal
свойства User используется метод расширения IsSuperUser(long userspaceId).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var canCreateWorkGroups = User.IsSuperUser(17);
// true, если пользователь является системным или администрирует данное пользовательское пространство.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
- если у пользователя в его Claim 'ах присутствует идентификатор системного пользователя;
- userspaceId -- 64-разрядное знаковое целое, идентификатор пользовательского пространства, администрирование которого проверяется. Например,
17
.
Метод возвращает true
:
- если пользователь является администратором данного пользовательского пространства;
Для проверки наличия конкретных прав исполнения в данной рабочей группе у пользователя запроса из ClaimsPrincipal
свойства User используется метод расширения HasGrant(long userspaceId, long workGroupId, string grantName).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var canReadRsm = User.HasGrant(17, 23, "base-system.rsm.read");
// true, если у пользователя есть такие права в данной рабочей группе.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
- userspaceId -- 64-разрядное знаковое целое, идентификатор пользовательского пространства, для которого проверяются соответствующие права. Например,
17
. - workGroupId -- 64-разрядное знаковое целое, идентификатор рабочей группы, в которой проверяются соответствующие права. Например,
23
. - grantName -- строка, трёхчленное определение имени пользовательского права. Например,
base-system.rsm.read
.
Метод возвращает true
:
- если у пользователя запроса есть запрашиваемые права;
- если вызван системным пользователем;
- если вызван администратором запрашиваемого пользовательского пространства;
Для проверки наличия какого-либо из прав исполнения в данной рабочей группе у пользователя запроса из ClaimsPrincipal
свойства User используется метод расширения HasAnyGrant(long userspaceId, long workGroupId, IEnumerable<string> grantNames).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var canReadRsm = User.HasAnyGrant(17, 23, new[] { "base-system.rsm.read", "base-system.rsm.write" });
// true, если у пользователя есть право записи или чтения в данной рабочей группе.
// случай, когда право на запись предполагает и право чтения.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
- userspaceId -- 64-разрядное знаковое целое, идентификатор пользовательского пространства, для которого проверяются соответствующие права. Например,
17
. - workGroupId -- 64-разрядное знаковое целое, идентификатор рабочей группы, в которой проверяются соответствующие права. Например,
23
. - grantNames -- переменное количество строк, трёхчленных определений имени пользовательского права. Например,
base-system.rsm.read
,base-system.rsm.write
.
Метод возвращает true
:
- если у пользователя запроса есть хотя бы одно из запрашиваемых прав;
- если вызван системным пользователем;
- если вызван администратором запрашиваемого пользовательского пространства;
Для проверки наличия всех перечисленных прав исполнения в данной рабочей группе у пользователя запроса из ClaimsPrincipal
свойства User используется метод расширения HasAllGrants(long userspaceId, long workGroupId, IEnumerable<string> grantNames).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var canAddTimelineRole = User.HasAllGrants(17, 23, new[] { "base-system.timeline.read", "base-system.work-group.roles-write" });
// true, если у пользователя есть все перечисленные права в данной рабочей группе.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
- userspaceId -- 64-разрядное знаковое целое, идентификатор пользовательского пространства, для которого проверяются соответствующие права. Например,
17
. - workGroupId -- 64-разрядное знаковое целое, идентификатор рабочей группы, в которой проверяются соответствующие права. Например,
23
. - grantNames -- переменное количество строк, трёхчленных определений имени пользовательского права. Например,
base-system.timeline.read
,base-system.work-group.roles-write"
.
Метод возвращает true
:
- если у пользователя запроса есть все запрашиваемые права;
- если вызван системным пользователем;
- если вызван администратором запрашиваемого пользовательского пространства;
Для получения идентификаторов рабочих групп данного пользовательского пространства, в которых у пользователя запроса из ClaimsPrincipal
свойства User есть конкретные права, используется метод расширения GetWorkGroupsWithGrant(long userspaceId, string grantName).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var workGroupsWithSetRsmGrant = User.GetWorkGroupsWithGrant(17, "base-system.rsm.read");
// перечисление идентификаторов рабочих групп.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
-
userspaceId - 64-разрядное знаковое целое, идентификатор пользовательского пространства, администрирование которого проверяется. Например,
17
. -
grantName - строка, трёхчленное определение имени пользовательского права. Например,
base-system.rsm.read
.
Метод возвращает значение типа перечисление 64-разрядных знаковых целых, IEnumerable<long>
.
Для получения идентификаторов рабочих групп данного пользовательского пространства, в которых у пользователя запроса из ClaimsPrincipal
свойства User есть хотя бы одно из перечисленных прав, используется метод расширения GetWorkGroupsWithAnyGrant(long userspaceId, IEnumerable<string> grantNames).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var workGroupsWithGetRsmGrant = User.GetWorkGroupsWithAnyGrant(17, new[] { "base-system.rsm.write", "base-system.rsm.read" });
// перечисление идентификаторов рабочих групп.
// случай, когда право на запись подразумевает права чтения.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
-
userspaceId - 64-разрядное знаковое целое, идентификатор пользовательского пространства, администрирование которого проверяется. Например,
17
. -
grantNames -- переменное количество строк, трёхчленных определений имени пользовательского права. Например,
base-system.rsm.write
,base-system.rsm.read
.
Метод возвращает значение типа перечисление 64-разрядных знаковых целых, IEnumerable<long>
.
Для получения идентификаторов рабочих групп данного пользовательского пространства, в которых у пользователя запроса из ClaimsPrincipal
свойства User есть все перечисленные права, используется метод расширения GetWorkGroupsWithAllGrants(long userspaceId, IEnumerable<string> grantNames).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var workGroupsWithUserAdministration = User.GetWorkGroupsWithAllGrants(17, new [] { "base-system.work-group.users-write", "base-system.work-group.roles-write" });
// перечисление идентификаторов рабочих групп.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргументы:
-
userspaceId - 64-разрядное знаковое целое, идентификатор пользовательского пространства, администрирование которого проверяется. Например,
17
. -
grantNames -- переменное количество строк, трёхчленных определений имени пользовательского права. Например,
base-system.work-group.users-write
,base-system.work-group.roles-write
.
Метод возвращает значение типа перечисление 64-разрядных знаковых целых, IEnumerable<long>
.
Для получения идентификаторов рабочих групп, в которых у пользователя запроса из ClaimsPrincipal
свойства User есть какие-либо права, используется метод расширения WorkGroups(long userspaceId).
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var userWorkGroups = User.WorkGroups(17);
// перечисление идентификаторов рабочих групп.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Аргумент:
- userspaceId - 64-разрядное знаковое целое, идентификатор пользовательского пространства, администрирование которого проверяется. Например,
17
.
Метод возвращает значение типа перечисление 64-разрядных знаковых целых, IEnumerable<long>
.
Для получения идентификаторов пространств пользователя, в рабочих группах которых у пользователя запроса из ClaimsPrincipal
свойства User есть какие-либо права, используется метод расширения Userspaces().
[Route("api/test")]
public class TestController : Controller
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var allUserspaces = User.Userspaces();
// перечисление идентификаторов пространств пользователя.
...
}
}
Свойство контроллера User унаследовано из
ControllerBase
в пространстве имёнMicrosoft.AspNetCore.Mvc
.
Метод возвращает значение типа перечисление 64-разрядных знаковых целых, IEnumerable<long>
.
Для упрощения модульного тестирования контроллеров и сервисов, которые используют библиотеку пользовательских прав, методы расширения (см. Реализуемые методы расширения) реализуют одноимённые методы интерфейса IGrantsExtensions
, таким образом, фасад библиотеки является полностью подменяемым.
Ниже следует инструкция по рекомендуемой подмене методов расширения в модульном тестировании методов контроллеров, в которых используются методы расширения библиотеки.
Начиная с версии 1.2.0
библиотека включает в себя эталонную имплементацию класса-подмены IGrantsExtensions
. Это сделано для достижения двух основных целей:
- избегания повторения однотипного кода в тестах проектов, использующих авторизацию (предполагается, что таких будет большинство);
- отсутствия необходимости вносить изменения в каждую из реализаций при дальнейших изменениях API.
Эталонная имплементация содержится в пространстве имён Monq.Core.Authorization.Tests
и реализуется классом FakeGrantsImpl
. Вызовы методов реализации интерфейса переназначены на соответствующие им функции, каждая из которых по умолчанию доступна как свойство класса. Правило именования содержащих функции свойств такое Имя метода расширения
+ Func
. Таким образом, для подмены метода расширения Subject() необходимо задать собственную реализацию SubjectFunc, переопределив её (см. ниже).
Кроме того, эталонная реализация включает метод Assign(), позволяющий назначить её в качестве текущей используемой реализации прав.
Пример:
public class FakeGrantsImpl : IGrantsExtensions
{
...
public Func<ClaimsPrincipal, long> SubjectFunc { get; set; }
public long Subject(ClaimsPrincipal user) => SubjectFunc(user);
public void Assign() => GrantsExtensions.Implementation = this;
...
}
Кроме того, реализуется свойство AutoAssign, которое может указывать используемой реализации функций на необходимость автоматического вызова метода Assign() при назначении. Его назначение вынесено в конструктор.
public class FakeGrantsImpl : IGrantsExtensions
{
...
public bool AutoAssign { get; set; }
public FakeGrantsImpl(bool autoAssign = false) => AutoAssign = autoAssign;
...
}
Ремарка В общем случае, использование AutoAssign будет считаться дурным тоном (антипаттерном), потому как реализует и полагается на побочный эффект; но в определённых ситуациях может быть полезным, поэтому такая возможность на страницах этого руководства не только упоминается, но и рассматривается.
Логика, т.е. подмена отсутствующих функций используемыми в данном тесте, в такой реализации выносится в отдельный класс с методами расширения -- уже на стороне тестовой библиотеки, потому что эталонной реализации таких методов быть не может. Тем не менее, начиная с версии 1.3.1
библиотеки эталонная реализация в случае отсутствия подменяемых функций обращается к реализации по умолчанию, поэтому некоторые тесты в корректном окружении могут не требовать подмены каждого из методов (как UseSubject() из примера ниже).
Такой подход позволяет более гибкое создание экземпляра подмены методов расширения прав в рамках "текучего интерфейса" (fluent interface, Martin Fowler) через цепочки методов (method chaining, Eric Evans) конструктора.
Минималистичными примерами полного игнорирования существующих свойств окружения могут быть такие методы:
public static class FakeGrantsExtensions
{
...
public static FakeGrantsImpl FakeSubject(this FakeGrantsImpl fakeGrants, long subjectId)
{
fakeGrants.SubjectFunc = (user) => subjectId;
return fakeGrants;
}
public static FakeGrantsImpl FakeHasGrant(this FakeGrantsImpl fakeGrants, bool value)
{
fakeGrants.HasGrantFunc = (user, workGroup, grantName) => value;
return fakeGrants;
}
...
}
Первый из методов устанавливает предполагаемый идентификатор пользователя предзаданным значением; второй устанавливает для любого вызова HasGrant() значение true
.
Стоит обратить внимание на то, что _все методы должны принимать экземпляр класса-реализации интерфейса IGrantsExtensions
(см. выше) и возвращать его же.
Более комплексным подходом будет (в данном контексте) использование реального субъекта запроса (который переопределяется где-то ещё) и задание конкретных прав этому субъекту. Поэтому реализацию методов расширения можно дополнить методами:
public static class FakeGrantsExtensions
{
...
const sbyte SystemUserId = -1;
const sbyte DefaultUserId = 0;
public static FakeGrantsImpl UseSubject(this FakeGrantsImpl fakeGrants)
{
fakeGrants.SubjectFunc = (user) =>
{
if (user is null)
return DefaultUserId;
var userSub = user.Claims.FirstOrDefault(x => x.Type == SubjectClaim)?.Value;
if (string.IsNullOrWhiteSpace(userSub))
{
var isSystemUser = IsSystemUser(user);
if (isSystemUser)
return SystemUserId;
return DefaultUserId;
}
if (!long.TryParse(userSub, out var userId))
return DefaultUserId;
return userId;
};
return fakeGrants;
}
...
}
В целом повторяет библиотечный метод получения идентификатора пользователя по его
ClaimsPrincipal
,0
при любой ошибке и-1
для системного пользователя.
и
public static class FakeGrantsExtensions
{
...
public static FakeGrantsImpl FakeHasGrant(this FakeGrantsImpl fakeGrants, long subjectId, long workGroupId, string grant)
{
fakeGrants.HasGrantFunc = (user, workGroup, grantName) =>
{
if (user.Subject() == subjectId
&& workGroup == workGroupId
&& grantName == grant)
return true;
return false;
};
return fakeGrants;
}
...
}
Позволяет задать, в какой рабочей группе у данного пользователя есть указанные права. Идентификаторы рабочей группы и пользователя, а также строковое представление прав передаются аргументами в параметры.
Внимание Данная реализация HasGrant() использует Subject(), поэтому для использования в текущем экземпляре подменных прав требуется имплементировать в т.ч. и подмену этого метода.
Для примера рассмотрим участок кода, навеянный описанием метода расширения HasGrant(long workGroupId, string grant), и гипотетический тест, который с помощью описанных выше техник могли бы написать.
Тестируемый участок будет выглядеть следующим образом:
[HttpGet]
public async Task<IActionResult> GetAll()
{
...
var workGroupId = _workGroupService.GetWorkGroupId();
var canReadRsm = User.HasGrant(workGroupId, "base-system.rsm.read");
if (!canReadRsm)
return StatusCode(StatusCodes.Status403Forbidden);
...
}
Ремарка Реализация существенно упрощена. Конечно, в реальном приложении инициализация переменных сложнее, а константы вынесены в отдельный класс или берутся из базы.
В этом примере мы каким-то чудесным образом получаем идентификатор рабочей группы и проверяем, есть ли у пользователя в ней права на чтение РСМ.
Опираясь на нашу реализацию, можно задать тестовые условия следующим образом:
[Fact(DisplayName = "TestController: GetAll: Проверка корректного получения тестовых данных.")]
public async Task ShouldProperlyGetAll()
{
var subjectId = _sporadic.GetId();
var workGroupId = await CreateWorkGroup(subjectId);
var fakeGrants = new FakeGrantsImpl()
.UseSubject()
.FakeHasGrant(subjectId, workGroupId, "base-system.rsm.read");
fakeGrants.Assign();
...
}
После вызова fakeGrants.Assign(); обращение к методу расширения User.HasGrant(workGroupId, "base-system.rsm.read"); будет перенаправлено в новый экземпляр FakeGrantsImpl
, где этот метод вызовет переопределённую нами функцию HasGrantFunc. Поскольку имплементация HasGrantFunc в нашем случае прямо обращается к другому методу расширения, Subject(), также переопределяемому расширением UseSubject(). Конечно, такая реализация возможна только при использовании тестов, переопределяющих пользователя контекста контроллера.
TestController CreateController(long subjectId)
{
var controller = new TestController();
controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() };
controller.HttpContext.User = new ClaimsPrincipal(
new ClaimsIdentity(
new Claim[] { new Claim(JwtClaimTypes.Subject, subjectId.ToString()) },
string.Empty, JwtClaimTypes.Name, JwtClaimTypes.Role));
return controller;
}
Для того, чтобы заменить используемую реализацию (статических) методов расширения для работы с авторизацией на имплементацию по умолчанию, достаточно вызвать RevertToDefaults()-метод FakeGrantsImpl
.
[Fact(DisplayName = "TestController: GetAll: Проверка корректного получения тестовых данных.")]
public async Task ShouldProperlyGetAll()
{.
var fakeGrants = new FakeGrantsImpl()
.FakeIsSystemUser(true); // Подменяется метод IsSystemUser()
fakeGrants.Assign();
... // Выполнение тестов
fakeGrants.RevertToDefaults(); // Использовать IsSystemUser() по умолнчаию
}
Главным недостатком такого подхода является невозможность последовательной семантической установки одной заменяемой функции, например:
var fakeGrants = new FakeGrantsImpl()
.UseSubject()
.FakeHasGrant(subjectId, workGroupId, "base-system.rsm.read")
.FakeHasGrant(subjectId, workGroupId, "base-system.work-group.read");
fakeGrants.Assign();
В этом примере будет установлено только второе право, а вызов User.HasGrant(workGroupId, "base-system.rsm.read") вернёт
false
.
В качестве решения для указанных ситуаций предлагаются перегрузки, принимающие множество прав, например:
public static FakeGrantsImpl FakeHasGrant(this FakeGrantsImpl fakeGrants, long subjectId, long workGroupId, IEnumerable<string> grants)
{
fakeGrants.HasGrantFunc = (user, workGroup, grantName) =>
{
if (user.Subject() == subjectId
&& workGroup == workGroupId
&& grants.Contains(grantName))
return true;
return false;
};
return fakeGrants;
}
Такая перегрузка позволит переписать прошлый пример следующим (работопригодным) образом:
var fakeGrants = new FakeGrantsImpl()
.UseSubject()
.FakeHasGrant(subjectId, workGroupId,
new[] { "base-system.rsm.read", "base-system.work-group.read" });
fakeGrants.Assign();
Другой альтернативой могут служить множественные экземпляры подмены прав, что чаще всего не имеет практического приложения в реальных тестах.
Предполагается, что добавление третьего уровня абстракции тестирования, а именно -- псевдоколлекции прав, позволит переопределить функции в FakeGrantsImpl
на почти аналогичные исходным, с тем лишь исключением, что ссылаться они будут на собственный репозиторий прав.
Вкупе с существенно возрастающей трудоёмкостью написания и поддержки таких тестов и незначительным выигрышем, получаемым взамен, чаще всего такой подход будет избыточным.
Другая альтернатива -- неявное хранилище прав в данном экземпляре FakeGrantsImpl
, которое заполняется вместе с переопределением функций, кажется более пригодной к использованию в реальных проектах.