Авторизація
Як застосунок вирішує, що можна вже автентифікованому користувачу: ролі й claims, політики з requirements і handlers, перевірка доступу до конкретного об’єкта, моделі ABAC і ReBAC та будова централізованої системи керування доступом. На співбесіді тут перевіряють і знання ASP.NET Core, і вміння спроєктувати права для десятків застосунків.
Перед цією темою: Основи вебзахисту, JWT і захист API.
Що таке рольова модель доступу (RBAC) і який вигляд вона має в ASP.NET Core?
Section titled “Що таке рольова модель доступу (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 ідентичність?
Section titled “Що таке 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 із правилами.
Як працює авторизація на політиках?
Section titled “Як працює авторизація на політиках?”Що це
Політика — правило доступу з назвою. Усередині — вимоги: «увійшов», «є роль», «є 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()));// Два handlers одного 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 і зовнішні рушії політик?
Section titled “Коли 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 і потрібен контекст користувача в з’єднанні.
Як спроєктувати централізовану систему керування доступом для багатьох внутрішніх застосунків?
Section titled “Як спроєктувати централізовану систему керування доступом для багатьох внутрішніх застосунків?”Що це
Система керування доступом — одне місце, де видно всі застосунки компанії й хто які права в них має.
Як влаштовано для працівника
- Обирає в каталозі застосунок і роль, пише обґрунтування.
- Заявку погоджують керівник і власник застосунку.
- Система видає права через групу AD або API застосунку.
- Усе пишеться в журнал: хто просив, хто погодив, коли видали.
Принципи
- Найменші привілеї — лише те, що потрібно для роботи.
- Термін — тимчасові права закінчуються самі.
- Перегляд — керівник регулярно підтверджує, що права ще потрібні.
Типова помилка
Видавати, але не забирати. Працівник змінив відділ, старі права лишилися, і після кількох переведень у нього доступ майже до всього — це накопичення прав.
Як влаштовано
- Каталог — застосунки, їхні права, ролі як набори прав, власники.
- Джерело працівників — кадрова система або AD: прийом, переведення, звільнення.
- Заявки — маршрут погодження залежить від ризику права: керівник, власник застосунку, служба безпеки.
- Видача — групи AD, протокол SCIM або API застосунку; повторний запуск не має дублювати права.
- Доставка в застосунок — claims під час входу або запит до сервісу прав.
- Аудит і перегляд — журнал усіх змін і регулярні кампанії підтвердження.
Коли що обирати
| Доставка прав | Плюс | Мінус |
|---|---|---|
| групи AD або claims у токені | без викликів, стандартно | зміни видно після нового входу |
| запит до API прав із кешем | свіжі й точні права | затримка, залежність від сервісу |
Пастки
- «Як у колеги». Копіювання прав колеги переносить і зайве.
- Переведення небезпечніше за звільнення. Під час звільнення блокують обліковий запис, а під час переведення старі права тихо накопичуються.
- Права в обхід системи. Видане вручну в застосунку система не бачить — потрібне звіряння.
Правило
Кожне право має власника, обґрунтування й термін, і все, що система видала, вона вміє відкликати.
Схема компонентів
- Каталог: застосунки, права, ролі-пакети, власники, рівень ризику.
- Синхронізація працівників із кадрової системи або AD: події прийому, переведення, звільнення.
- Рушій заявок: маршрут за ризиком і перевірка розділення обов’язків до погодження.
- Сховище призначень — бажаний стан: хто, що, підстава, термін.
- Provisioning: конектори до 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.
- Що робити, якщо сервіс прав недоступний? — Зміни забороняти, а для читання заздалегідь обрати: короткий застарілий кеш або відмова.