Авторизация
Как приложение решает, что можно уже аутентифицированному пользователю: роли и claims, политики с requirements и handlers, проверка доступа к конкретному объекту, модели ABAC и ReBAC и устройство централизованной системы управления доступом. На собеседовании здесь проверяют и знание ASP.NET Core, и умение спроектировать права для десятков приложений.
Перед этой темой: Основы веб-безопасности, JWT и защита API.
Что такое ролевая модель доступа (RBAC) и как она выглядит в ASP.NET Core?
Заголовок раздела «Что такое ролевая модель доступа (RBAC) и как она выглядит в ASP.NET Core?»Что это
RBAC — модель доступа в два шага: права собирают в роли, а пользователям назначают роли. Роль «Бухгалтер» даёт просмотр и проведение платежей, и новому бухгалтеру выдают её, а не двадцать прав.
Зачем
Доступом управляют группами, а не каждым пользователем, и по составу роли видно, кто что может.
Как пользоваться
[Authorize]— только вошедшие пользователи.[Authorize(Roles = "Admin,Manager")]— роль Admin или Manager.- Два атрибута
[Authorize(Roles = ...)]с разными ролями — нужны обе. [AllowAnonymous]— доступ без входа: страница логина.- В Minimal API —
RequireAuthorization()иAllowAnonymous()у эндпоинта или группыMapGroup.
Частая ошибка
Забыть [Authorize] на новом контроллере: эндпоинт без атрибута открыт всем. Защищает fallback policy, которая требует входа везде, где не сказано иное.
Как устроено
- Аутентификация заполняет
HttpContext.User; роли в нём — claims специального типа. - Middleware авторизации собирает метаданные эндпоинта —
[Authorize],RequireAuthorization— в одну политику. Rolesпревращается в требование:User.IsInRoleистинно хотя бы для одной роли из списка.- Если требование не выполнено, аноним получает challenge (предложение войти), а вошедший — forbid (отказ).
Какая политика применится
| Метаданные эндпоинта | Что проверяется |
|---|---|
| нет | fallback policy; по умолчанию её нет, и доступ открыт |
[Authorize], RequireAuthorization() |
default policy: пользователь вошёл |
Roles = "..." или Policy = "..." |
роли или именованная политика |
[AllowAnonymous], AllowAnonymous() |
ничего: отказа не будет |
Ловушки
[AllowAnonymous]сильнее. На контроллере он отменяет все[Authorize]его actions.- Регистр. Роли сравниваются с учётом регистра:
Adminиadmin— разные роли. - Строки в коде. Опечатку в имени роли компилятор не поймает, а переименование роли превращается в поиск по всему проекту.
Правило
Fallback policy с RequireAuthenticatedUser, явный AllowAnonymous для публичного, а в атрибутах — имена политик вместо списков ролей.
Под капотом
- Модель NIST RBAC (стандарт ANSI INCITS 359) — это базовый уровень (пользователи, роли, права, сессии), иерархии ролей и ограничения: статическое и динамическое разделение обязанностей. ASP.NET Core даёт только базовый уровень, остальное строят снаружи.
Rolesпревращается вRolesAuthorizationRequirement, аIsInRoleищет в каждой identity claim с типом еёRoleClaimType. Если провайдер кладёт роли в claim с другим именем,IsInRoleвернётfalseпри правильных ролях.
Компромиссы
- Роль или право в коде.
[Authorize(Roles = "HR,Admin")]смешивает «кто» и «что можно». ПолитикаCanApproveVacationпрячет состав ролей в одном месте, и новая роль не требует правки контроллеров. - Роли в токене или cookie. Проверка ничего не стоит, но снятая роль действует до перевыпуска токена или конца сессии.
- Безопасность по умолчанию. Fallback policy делает забытый атрибут отказом. Цена — каждое публичное место надо пометить явно: вход, health checks, публичные страницы и их статику.
Как проверить в проде
- Интеграционный тест через
WebApplicationFactoryперебираетEndpointDataSourceи падает, если у эндпоинта нет ни метаданных авторизации, ниAllowAnonymous. - При отказе категория логов
Microsoft.AspNetCore.Authorizationпишет «Authorization failed» и список невыполненных требований.
Итог: [Authorize(Roles)] — базовый RBAC поверх claims; в проде к нему добавляют fallback policy, политики вместо строк ролей и учёт того, что роли в токене устаревают.
using System.Security.Claims;using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddCookie(); // единственная схема становится схемой по умолчаниюbuilder.Services.AddAuthorizationBuilder() // Всё, что не помечено явно, требует входа .SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
var app = builder.Build();
// Публичный эндпоинт: вход тестового пользователя с ролью Managerapp.MapGet("/login", () => Results.SignIn(new ClaimsPrincipal(new ClaimsIdentity( [new Claim(ClaimTypes.Name, "alice"), new Claim(ClaimTypes.Role, "Manager")], "Cookies")))) .AllowAnonymous();
app.MapGet("/profile", (ClaimsPrincipal user) => user.Identity!.Name); // fallback: любой вошедший
// Атрибут на лямбде: Admin ИЛИ Manager — alice проходитapp.MapGet("/reports", [Authorize(Roles = "Admin,Manager")] () => "reports");
// Всем эндпоинтам группы нужна роль Admin — alice получит отказvar admin = app.MapGroup("/admin").RequireAuthorization(p => p.RequireRole("Admin"));admin.MapGet("/users", () => "users");
app.Run();- Как потребовать две роли одновременно? — Два атрибута
[Authorize(Roles = ...)]или два вызоваRequireRoleв политике; список через запятую — это ИЛИ. - Чем default policy отличается от fallback policy? — Default применяется к
[Authorize]без параметров, fallback — к эндпоинтам без метаданных авторизации. - Что победит:
[Authorize]на контроллере или[AllowAnonymous]на action? —[AllowAnonymous]: с ним атрибуты[Authorize]игнорируются. - Почему
IsInRoleвозвращаетfalse, хотя роль в токене есть? — Claim роли пришёл с другим типом, чемRoleClaimTypeidentity; это настраивается в схеме аутентификации. - Как закрыть сразу группу эндпоинтов? —
MapGroup(...).RequireAuthorization(...)в Minimal API или атрибут на контроллере.
Что такое claims-based идентичность?
Заголовок раздела «Что такое claims-based идентичность?»Что это
- Claim — пара «тип — значение» о пользователе:
name = alice,department = HR,role = Manager. ClaimsIdentity— claims, полученные одним способом входа: из cookie, токена или Windows.ClaimsPrincipal— пользователь целиком, одна или несколько identity. ЭтоHttpContext.User.
Откуда берутся
Их выдаёт тот, кто аутентифицировал пользователя. Форма входа записывает claims в cookie, сервер идентификации — в токен, Windows-аутентификация берёт их из учётной записи домена.
Как пользоваться
User.Identity.Name— имя,User.IsInRole("Admin")— роль.User.FindFirst(ClaimTypes.Email)?.Value— значение claim.User.HasClaim("department", "HR")— проверка значения.
Частая ошибка
Класть в claims быстро меняющиеся данные: остаток отпуска, список заявок. Они не обновятся, пока пользователь не войдёт заново.
Как устроено
| Тип | Что содержит |
|---|---|
Claim |
Type, Value, ValueType и Issuer — кто выдал |
ClaimsIdentity |
claims одной аутентификации, AuthenticationType, NameClaimType, RoleClaimType |
ClaimsPrincipal |
список identity; свойство Claims — claims всех identity сразу |
- Identity считается аутентифицированной, если у неё задан
AuthenticationType.new ClaimsIdentity(claims)без него — аноним с claims. - Типы claims — длинные URI из
ClaimTypesили короткие имена JWT и OIDC:sub,email,role. Как одни превращаются в другие, решает схема аутентификации.
Роли — частный случай
IsInRole("Admin") ищет в каждой identity claim с типом её RoleClaimType и значением Admin. Через него работают [Authorize(Roles)] и RequireRole.
Дополнение claims
После успешной аутентификации ASP.NET Core вызывает IClaimsTransformation.TransformAsync. Там добавляют данные приложения: внутренний id сотрудника, отдел из кадровой системы.
Ловушки
TransformAsyncможет вызываться несколько раз за запрос: claim добавляют, только если его ещё нет.- Результат не сохраняется в cookie и вычисляется на каждом запросе, поэтому обращение к базе в нём кешируют.
- Значения claims сравниваются с учётом регистра.
Правило
В токене или cookie — устойчивые сведения о личности, а данные приложения добавляют в самом приложении.
Под капотом
AuthenticationService.AuthenticateAsyncвызывает обработчик схемы, а затемIClaimsTransformation. Middleware аутентификации делает это для схемы по умолчанию, а политика с явнымиAuthenticationSchemes— ещё раз для своих схем.- ASP.NET Core Identity собирает claims при входе через
IUserClaimsPrincipalFactory<TUser>. Дальше они живут в cookie и обновляются, только когда cookie перевыпускают, например валидатором security stamp — по умолчанию раз в 30 минут. WindowsPrincipalиGenericPrincipalнаследуютClaimsPrincipal, поэтому любой способ входа даёт приложению один и тот же API.
Цена
- Каждый claim увеличивает cookie или токен, а они едут в каждом запросе. Большую cookie ASP.NET Core делит на части (
.AspNetCore.CookiesC1,C2), но заголовки всё равно упираются в лимиты прокси и сервера. - Трансформация с запросом к базе добавляет задержку каждому запросу, поэтому результат кешируют по id пользователя с коротким TTL.
Компромиссы
| Источник данных для решения | Плюс | Минус |
|---|---|---|
| claims в cookie или токене | без сетевых вызовов | устаревают до перевыпуска |
| трансформация с кешем | свежие данные приложения | кеш или база на каждый запрос |
| запрос в момент проверки | точное решение по объекту | задержка в момент проверки |
Как проверить в проде
- Эндпоинт для администраторов, который отдаёт
User.Claims, показывает типы, издателей и то, что добавила трансформация. - Размер заголовков
CookieиAuthorizationв логах прокси — ранний сигнал, что claims разрослись.
Итог: claims — снимок утверждений издателя; держите в них устойчивое, а изменчивое добавляйте трансформацией с кешем или проверяйте в момент решения.
using System.Security.Claims;using Microsoft.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddCookie();builder.Services.AddAuthorizationBuilder() .AddPolicy("HrOnly", p => p.RequireClaim("department", "HR"));builder.Services.AddSingleton<EmployeeDirectory>();builder.Services.AddTransient<IClaimsTransformation, DepartmentClaimsTransformation>();
var app = builder.Build();
// В cookie попадают только имя и рольapp.MapGet("/login", () => Results.SignIn(new ClaimsPrincipal(new ClaimsIdentity( [new Claim(ClaimTypes.Name, "alice"), new Claim(ClaimTypes.Role, "Manager")], "Cookies"))));
// name, role и department, который добавила трансформацияapp.MapGet("/me", (ClaimsPrincipal user) => user.Claims.Select(c => new { c.Type, c.Value })) .RequireAuthorization();app.MapGet("/hr/vacancies", () => "vacancies").RequireAuthorization("HrOnly"); // alice из HR — проходит
app.Run();
public class EmployeeDirectory{ public string GetDepartment(string userName) => userName == "alice" ? "HR" : "IT";}
// Вызывается после каждой успешной аутентификации, иногда несколько раз за запросpublic class DepartmentClaimsTransformation(EmployeeDirectory directory) : IClaimsTransformation{ public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { if (principal.Identity?.Name is not { } name || principal.HasClaim(c => c.Type == "department")) return Task.FromResult(principal); // аноним или claim уже есть: ничего не делаем
var clone = principal.Clone(); // исходный principal не меняем clone.AddIdentity(new ClaimsIdentity([new Claim("department", directory.GetDepartment(name))])); return Task.FromResult(clone); }}- Чем
ClaimsIdentityотличается отClaimsPrincipal? — Identity — claims одной аутентификации, principal — пользователь с одной или несколькими identity. - Почему
IsAuthenticatedравноfalse, хотя claims есть? — У identity не заданAuthenticationType. - Сколько раз вызывается
IClaimsTransformation? — На каждыйAuthenticateAsync, иногда несколько раз за запрос; результат в cookie не сохраняется. - Как обновить claims, не дожидаясь нового входа? — Перевыпустить cookie через
SignInAsyncили токен; в ASP.NET Core Identity это делает валидатор security stamp. - Чем claim отличается от права? — Claim говорит, кто субъект, право — что ему можно; решение принимает приложение, сопоставляя claims с правилами.
Как работает авторизация на политиках?
Заголовок раздела «Как работает авторизация на политиках?»Что это
Политика — правило доступа с именем. Внутри — требования: «вошёл», «есть роль», «есть claim» или своё условие.
Как пользоваться
- Зарегистрируйте политику:
AddAuthorizationBuilder().AddPolicy("HrOnly", p => p.RequireClaim("department", "HR")). - Примените её:
[Authorize(Policy = "HrOnly")]на контроллере или.RequireAuthorization("HrOnly")у эндпоинта. - Для сложного правила напишите свой requirement и handler — класс, который принимает решение.
Зачем
Правило описано один раз. Если доступ нужно дать ещё и аудиторам, правят регистрацию политики, а не все атрибуты.
Частая ошибка
Не зарегистрировать handler в DI. Requirement тогда никто не обработает, и политика всегда отказывает.
Как устроено
AddAuthorizationBuilder()(.NET 7+) регистрирует политики; каждая — списокIAuthorizationRequirement.- При проверке
IAuthorizationServiceсоздаётAuthorizationHandlerContext: пользователь, requirements и ресурс. - Вызываются все зарегистрированные
IAuthorizationHandler. Handler вызываетcontext.Succeed(requirement),context.Fail()или ничего. - Политика выполнена, если успешны все requirements и никто не вызвал
Fail.
Как сочетаются
| Ситуация | Результат |
|---|---|
| несколько requirements в политике | нужны все (И) |
| несколько handlers одного requirement | хватит одного Succeed (ИЛИ) |
хотя бы один Fail() |
отказ, даже если другие вызвали Succeed |
Авторизация по ресурсу
Атрибут проверяется до загрузки данных. Поэтому правило «правит только автор» проверяют в коде: загрузить документ, вызвать AuthorizeAsync(User, document, "EditDocument") и при отказе вернуть Forbid(). Handler наследует AuthorizationHandler<TRequirement, Document>; для CRUD есть готовый OperationAuthorizationRequirement.
Ловушки
- Handlers вызываются в любом порядке и даже для неаутентифицированного пользователя.
- У эндпоинта
context.Resource— этоHttpContext, а не ваш объект. - Опечатка в имени политики —
InvalidOperationExceptionпри первом запросе, а не при старте.
Правило
Простое условие — RequireRole, RequireClaim, RequireAssertion. Нужны сервисы или объект — requirement и handler.
Под капотом
AuthorizationMiddlewareобъединяет метаданные эндпоинта в одну политику, аIPolicyEvaluatorаутентифицирует пользователя по её схемам и вызываетIAuthorizationService.DefaultAuthorizationHandlerProviderотдаёт всеIAuthorizationHandlerиз DI: каждый handler вызывается при каждой проверке, аAuthorizationHandler<T>просто пропускает чужие requirements.- Результат проверки на эндпоинте обрабатывает
IAuthorizationMiddlewareResultHandler: по умолчанию challenge или forbid. Своя реализация меняет ответ, например 404 вместо 403. IAuthorizationPolicyProviderстроит политики по имени на лету ("MinAge:18"), а с .NET 8 атрибут сIAuthorizationRequirementDataсам несёт requirements без регистрации политики.
Цена
Результат handlers не кешируется: запрос к базе или сервису прав внутри handler’а выполняется при каждой проверке. Handler регистрируют с любым временем жизни; scoped получает DbContext запроса.
Компромиссы
- Атрибут или вызов в коде. Атрибут виден в метаданных, и его наличие проверяется тестом. Вызов
AuthorizeAsyncлегко забыть в новом методе. - Объект или список.
AuthorizeAsyncпроверяет один загруженный объект. Для списков права выражают вWHERE, иначе ломаются пагинация и производительность. - 404 или 403. 404 скрывает существование ресурса, 403 понятнее клиенту и поддержке.
Как проверить в проде
- Логи категории
Microsoft.AspNetCore.Authorizationпоказывают, какие requirements не выполнены. - Handlers тестируют без веб-хоста: создают
AuthorizationHandlerContextс нужным principal и ресурсом и вызываютHandleAsync.
Итог: политики — встроенный движок правил ASP.NET Core: требования декларативно, решения в handlers, объект — через IAuthorizationService, списки — фильтром в запросе.
using System.Security.Claims;using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddCookie();builder.Services.AddAuthorizationBuilder() .AddPolicy("EditDocument", p => p.AddRequirements(new EditDocumentRequirement()));// Два handler'а одного requirement: хватит успеха любого (ИЛИ)builder.Services.AddSingleton<IAuthorizationHandler, AuthorHandler>();builder.Services.AddSingleton<IAuthorizationHandler, AdminHandler>();
var app = builder.Build();
app.MapPut("/documents/{id:int}", async (int id, ClaimsPrincipal user, IAuthorizationService auth) =>{ var doc = new Document(id, Author: "bob"); // в жизни — загрузка из базы var result = await auth.AuthorizeAsync(user, doc, "EditDocument"); return result.Succeeded ? Results.Ok(doc) : Results.Forbid(); // не автор и не Admin — Forbid}).RequireAuthorization();
app.Run();
public record Document(int Id, string Author);
public class EditDocumentRequirement : IAuthorizationRequirement { }
// Handler с ресурсом: получает загруженный документpublic class AuthorHandler : AuthorizationHandler<EditDocumentRequirement, Document>{ protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, EditDocumentRequirement requirement, Document doc) { if (context.User.Identity?.Name == doc.Author) context.Succeed(requirement); return Task.CompletedTask; }}
public class AdminHandler : AuthorizationHandler<EditDocumentRequirement>{ protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, EditDocumentRequirement requirement) { if (context.User.IsInRole("Admin")) context.Succeed(requirement); return Task.CompletedTask; }}- Как сделать правило «ИЛИ» между двумя условиями? — Несколько handlers одного requirement или
RequireAssertionс||. - Зачем вызывать
context.Fail(), если можно просто не вызыватьSucceed? —Failгарантирует отказ, даже если другой handler вызвалSucceed, например для заблокированного пользователя. - Почему проверку «автор документа» нельзя сделать атрибутом? — Атрибут срабатывает до загрузки документа; ресурс передают в
AuthorizeAsyncявно. - Как вернуть 404 вместо 403 при отказе в middleware? — Своя реализация
IAuthorizationMiddlewareResultHandler. - Как получить политику по шаблону имени, например
MinAge:18? — СвойIAuthorizationPolicyProviderили атрибут сIAuthorizationRequirementData(.NET 8+).
Когда RBAC не хватает: ABAC, ReBAC и внешние движки политик?
Заголовок раздела «Когда RBAC не хватает: ABAC, ReBAC и внешние движки политик?»Что это
- Взрыв ролей — под каждое сочетание должности, отдела и региона заводят роль:
HR_Kyiv_Manager,HR_Lviv_Managerи так сотни. - ABAC (attribute-based) — доступ решает правило над атрибутами: пользователя (отдел, должность), объекта (отдел, гриф), действия и обстановки (время, сеть).
- ReBAC (relationship-based) — доступ решают связи: Аня — редактор папки, папка содержит документ, значит, Аня может его править.
Пример
| Правило | Модель |
|---|---|
| админка — только администраторам | RBAC |
| менеджер видит заявки своего отдела | ABAC |
| документ видят те, с кем им поделились | ReBAC |
Частая ошибка
Решать каждое новое правило новой ролью. Через год ролей больше, чем пользователей, и никто не помнит, что даёт каждая.
Как устроено
- ABAC. По NIST SP 800-162 атрибуты субъекта, объекта, операции и окружения сравниваются с политикой. В ASP.NET Core это handler, который сравнивает claim
departmentпользователя с отделом заявки. - ReBAC. Права выводятся из графа. Zanzibar хранит связи кортежами
объект#отношение@пользователь, напримерdoc:readme#owner@10, а конфигурация задаёт вывод: «читатель папки — читатель и её документов». - OPA (Open Policy Agent) — универсальный движок: приложение отправляет JSON с данными запроса, а правила на языке Rego возвращают решение.
Когда что выбирать
| Модель | Подходит, когда |
|---|---|
| RBAC | права определяются должностью, ролей десятки |
| ABAC | правила «своё»: отдел, регион, гриф, время |
| ReBAC | шаринг, вложенные папки и группы, делегирование |
Где проверять
- Шлюз — грубо: вошёл ли пользователь и есть ли у него доступ к сервису.
- Сервис — точно: этот пользователь и этот объект.
- База — списки: условие в
WHEREили row-level security.
Ловушки
- Проверить один объект легко, а показать «всё, что мне доступно» — нет: фильтр после выборки ломает пагинацию.
- Атрибуты пользователя берут из claims или каталога, а не из параметров запроса.
Правило
Начинайте с ролей и политик, ABAC реализуйте в handlers, внешний движок подключайте, когда правила общие для многих сервисов.
Под капотом
- NIST SP 800-162 делит ABAC на точки: PEP применяет решение, PDP принимает его, PIP поставляет атрибуты, PAP хранит политики. В ASP.NET Core middleware и вызовы
AuthorizeAsync— PEP, handlers — встроенный PDP. - Zanzibar (USENIX ATC 2019) обслуживает Drive, YouTube, Cloud: триллионы ACL и миллионы проверок в секунду. Против «new enemy problem» — отозванный пользователь видит контент, добавленный после отзыва, — клиент хранит с контентом токен версии (zookie), и проверка идёт по данным не старше него. В SpiceDB это ZedToken.
Цена
- Внешний PDP — сетевой вызов на каждую проверку: его ставят рядом с сервисом и кешируют решения.
- «Что мне доступно» — отдельный дорогой запрос:
ListObjectsв OpenFGA,LookupResourcesв SpiceDB. - Отношения дублируются в базе приложения и в графе: нужны синхронизация и сверка.
Компромиссы
| Где проверять | Плюс | Минус |
|---|---|---|
| шлюз | один раз для всех сервисов | не знает данных объекта |
| сервис | полный контекст объекта | правила дублируются по сервисам |
| база (RLS) | защищает от забытой проверки в коде | логика в SQL, нужен контекст пользователя |
RLS требует передать пользователя в соединение: set_config в PostgreSQL, sp_set_session_context в SQL Server. С пулом соединений значение задают на транзакцию, иначе оно достанется следующему запросу.
Как проверить в проде
- Каждое решение логируют: кто, какое действие, над каким объектом, результат, версия политики.
- Политики тестируют в CI:
opa testдля Rego,fga model testдля модели OpenFGA.
Итог: RBAC не исчезает, а дополняется: атрибуты — в handlers, отношения — в графе вроде OpenFGA, списки — в запросе или RLS, а внешний PDP оправдан, когда правила общие для многих сервисов.
using System.Security.Claims;using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddCookie();builder.Services.AddSingleton(TimeProvider.System);builder.Services.AddSingleton<IAuthorizationHandler, ApproveRequestHandler>();builder.Services.AddAuthorizationBuilder() .AddPolicy("ApproveRequest", p => p.AddRequirements(new ApproveRequirement()));
var app = builder.Build();
app.MapPost("/requests/{id:int}/approve", async (int id, ClaimsPrincipal user, IAuthorizationService auth) =>{ var request = new AccessRequest(id, Department: "HR", RequestedBy: "bob"); // из базы var result = await auth.AuthorizeAsync(user, request, "ApproveRequest"); return result.Succeeded ? Results.Ok() : Results.Forbid();}).RequireAuthorization();
app.Run();
public record AccessRequest(int Id, string Department, string RequestedBy);
public class ApproveRequirement : IAuthorizationRequirement { }
// ABAC: решение из атрибутов субъекта, объекта и окруженияpublic class ApproveRequestHandler(TimeProvider time) : AuthorizationHandler<ApproveRequirement, AccessRequest>{ protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, ApproveRequirement requirement, AccessRequest request) { var user = context.User; var hour = time.GetLocalNow().Hour; if (user.IsInRole("Manager") // субъект: роль && user.HasClaim("department", request.Department) // субъект и объект: отдел && user.Identity?.Name != request.RequestedBy // объект: не своя заявка && hour is >= 8 and < 20) // окружение: рабочее время context.Succeed(requirement); return Task.CompletedTask; }}-- Приложение подключается не владельцем таблицы: владелец обходит RLSALTER TABLE access_requests ENABLE ROW LEVEL SECURITY;
-- Без значения app.department_id условие даёт NULL, и строк не видноCREATE POLICY same_department ON access_requests USING (department_id = current_setting('app.department_id', true)::int);
BEGIN;-- true: значение действует до конца транзакции и не достанется следующему запросу из пулаSELECT set_config('app.department_id', '42', true);SELECT * FROM access_requests; -- только заявки отдела 42COMMIT;- Как заметить взрыв ролей? — Ролей сопоставимо с пользователями, и роли отличаются одним параметром — его пора вынести в атрибут.
- Чем ABAC отличается от ReBAC? — ABAC сравнивает свойства, ReBAC проходит по связям в графе; «владелец» и «поделились» естественнее в ReBAC.
- Что такое new enemy problem? — Проверка по устаревшим ACL показывает отозванному пользователю новый контент; Zanzibar решает её токеном версии (zookie).
- Как показать список доступных объектов в ReBAC? —
ListObjectsв OpenFGA илиLookupResourcesв SpiceDB, либо копия прав в базе приложения для фильтрации. - Чем RLS лучше фильтра в коде и чем хуже? — Его не обойти забытым
WHERE, но логика уходит в SQL и нужен контекст пользователя в соединении.
Как спроектировать централизованную систему управления доступом для многих внутренних приложений?
Заголовок раздела «Как спроектировать централизованную систему управления доступом для многих внутренних приложений?»Что это
Система управления доступом — одно место, где видны все приложения компании и кто какие права в них имеет.
Как устроено для сотрудника
- Выбирает в каталоге приложение и роль, пишет обоснование.
- Заявку согласуют руководитель и владелец приложения.
- Система выдаёт права через группу AD или API приложения.
- Всё пишется в журнал: кто просил, кто согласовал, когда выдали.
Принципы
- Наименьшие привилегии — только то, что нужно для работы.
- Срок — временные права заканчиваются сами.
- Пересмотр — руководитель регулярно подтверждает, что права ещё нужны.
Частая ошибка
Выдавать, но не забирать. Сотрудник сменил отдел, старые права остались, и через несколько переводов у него доступ почти ко всему — это накопление прав.
Как устроено
- Каталог — приложения, их права, роли как наборы прав, владельцы.
- Источник сотрудников — кадровая система или AD: приём, перевод, увольнение.
- Заявки — маршрут согласования зависит от риска права: руководитель, владелец приложения, служба безопасности.
- Выдача — группы AD, протокол SCIM или API приложения; повторный запуск не должен дублировать права.
- Доставка в приложение — claims при входе или запрос к сервису прав.
- Аудит и пересмотр — журнал всех изменений и регулярные кампании подтверждения.
Когда что выбирать
| Доставка прав | Плюс | Минус |
|---|---|---|
| группы AD или claims в токене | без вызовов, стандартно | изменения видны после нового входа |
| запрос к API прав с кешем | свежие и точные права | задержка, зависимость от сервиса |
Ловушки
- «Как у коллеги». Копирование прав коллеги переносит и лишнее.
- Перевод опаснее увольнения. При увольнении блокируют учётную запись, а при переводе старые права тихо копятся.
- Права в обход системы. Выданное вручную в приложении система не видит — нужна сверка.
Правило
У каждого права есть владелец, обоснование и срок, и всё, что система выдала, она умеет отозвать.
Схема компонентов
- Каталог: приложения, права, роли-пакеты, владельцы, уровень риска.
- Синхронизация сотрудников из кадровой системы или AD: события приёма, перевода, увольнения.
- Движок заявок: маршрут по риску и проверка разделения обязанностей до согласования.
- Хранилище назначений — желаемое состояние: кто, что, основание, срок.
- Провижининг: коннекторы к AD, SCIM и API через очередь с повторами.
- Сверка: сравнение желаемого и фактического, поиск прав, выданных в обход.
- API решений и события «права изменились» для кешей приложений.
- Неизменяемый журнал аудита.
- Кампании пересмотра и автоматический отзыв по сроку.
Компромиссы
- Отзыв. С claims в токене он доходит только через время жизни токена или сессии. Сокращают жизнь токена или проверяют версию прав при обновлении сессии.
- Онлайн-проверка. Кеш в приложении с TTL в минуты плюс событие для сброса. Если сервис прав недоступен, изменения запрещают (fail closed), а для чтения выбор между устаревшим кешем и отказом согласуют с безопасностью.
- Разделение обязанностей. Конфликтные пары прав («создаёт платёж» и «утверждает платёж») проверяют при заявке; без владельцев эти правила быстро устаревают.
Как проверить в проде
- Время от согласования до фактической выдачи и число прав без владельца.
- Расхождения сверки: права в приложениях, которых нет в системе.
- Доля прав, подтверждённых на пересмотре без изменений: стопроцентное «подтвердить всё» — признак формальности.
- Журнал отвечает на вопрос «кто имел доступ к X на дату Y»; если нет — аудит неполный.
Итог: централизованный доступ — это желаемое состояние и циклы, которые сводят к нему реальность; главное архитектурное решение — как и с какой задержкой права доходят до приложений.
using Microsoft.AspNetCore.Authorization;using Microsoft.Extensions.Caching.Memory;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddCookie();builder.Services.AddMemoryCache();builder.Services.AddSingleton<PermissionClient>();builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();builder.Services.AddAuthorizationBuilder() .AddPolicy("requests.approve", p => p.AddRequirements(new PermissionRequirement("requests.approve")));
var app = builder.Build();
app.MapPost("/requests/{id:int}/approve", (int id) => $"Request {id} approved") .RequireAuthorization("requests.approve");
app.Run();
public record PermissionRequirement(string Permission) : IAuthorizationRequirement;
public class PermissionClient{ // В жизни — HttpClient к сервису прав: GET /users/{user}/permissions?app=hr-portal public Task<string[]> GetAsync(string user) => Task.FromResult<string[]>(["requests.approve"]);}
public class PermissionHandler(PermissionClient client, IMemoryCache cache) : AuthorizationHandler<PermissionRequirement>{ protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, PermissionRequirement requirement) { if (context.User.Identity?.Name is not { } user) return;
// Кеш на минуту: компромисс между нагрузкой на сервис прав и скоростью отзыва var permissions = await cache.GetOrCreateAsync($"perm:{user}", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); return client.GetAsync(user); }); if (permissions?.Contains(requirement.Permission) == true) context.Succeed(requirement); }}
// Подписчик на событие «права изменились»: отзыв доходит быстрее TTLpublic class PermissionsChangedHandler(IMemoryCache cache){ public void Handle(string user) => cache.Remove($"perm:{user}");}- Как быстро отозвать доступ, если права приходят в токене? — Короткая жизнь access token, отзыв refresh token и проверка версии прав; для cookie — перепроверка principal в
OnValidatePrincipal. - Что такое разделение обязанностей и где его проверять? — Запрет конфликтных прав у одного человека; статически при заявке и динамически при выполнении операции.
- Чем роль в каталоге отличается от группы AD? — Роль — набор прав с владельцем и правилами выдачи, группа — один из способов доставить её в приложение.
- Как выдать временный доступ администратора? — Право с датой окончания или активация по запросу на несколько часов, как в Microsoft Entra PIM.
- Что делать, если сервис прав недоступен? — Изменения запрещать, а для чтения заранее выбрать: короткий устаревший кеш или отказ.