Используем: .NET 9 (в дальнейшем сразу уходим в 10), EF.Core.
Задача такая:
Есть пользователи, есть cliams (в дальнейшем клеймы), а так же есть организации. Каждому пользователю можно назначить сколько угодно клеймов, а так же сколько угодно организаций. Сервер авторизации будет самописный на JWT.
Пользователь должен иметь доступ только к тем данным, к которым у него, соответственно, есть доступ через организацию. Например: дома, помещения в домах, лицевых счета абонентов в помещениях. Доступ организации выдается через контракт на дом (в редких случаях дом+помещение). Соответственно, если пользователь делает 1 запрос на получение лицевых счетов в доме, то он передает houseId, по которому нужно проверить, что у этого дома есть контракт на ту организацию, от которой работает пользователь (мы планируем выдавать JWT с Id'ами организаций прям в нем, для минимизирования запросов в бд на апи). У пользователя может быть, например, 2 организации с id'ами: 1, 2. У организации на дом №1 будет договор, а на №2 - нет. Значит пользователь сможет с организацией №1 видеть только информацию по дому №1, в т.ч. помещения, лицевые и прочее.
Как лучше сделать аутентификацию на стороне всех АПИ (у нас будут микросервисы), чтобы автоматически проверять доступ к ресурсу на основании id организации?
Пока выбор пал на IAsyncAuthorizationFilter + IAuthorizationRequirement + TypeFilterAttribute + AuthorizationHandler.
Пример того, что уже работает, но не факт, что это хорошо:
abstract class EntityAccessRequirementBase : IAuthorizationRequirement
{
public string RouteParameterName { get; }
protected EntityAccessRequirementBase(string routeParameterName)
{
RouteParameterName = routeParameterName;
}
}
class EntityAccessFilter : IAsyncAuthorizationFilter
{
private readonly IAuthorizationService _authorizationService;
private readonly EntityAccessRequirementBase _requirement;
public EntityAccessFilter(IAuthorizationService authorizationService, EntityAccessRequirementBase requirement)
{
_authorizationService = authorizationService;
_requirement = requirement;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
string? rawId = null;
if (context.HttpContext.Request.Query.TryGetValue(_requirement.RouteParameterName, out var queryVal))
{
rawId = queryVal.ToString();
}
if (!Guid.TryParse(rawId, out var id))
{
context.Result = new ForbidResult();
return;
}
if (context.HttpContext.User.IsInRole("Admin")) //тут пользователь с ролью "Admin" имеет доступ ко всем ресурсам вообще
{
return;
}
var result = await _authorizationService.AuthorizeAsync(context.HttpContext.User, id, _requirement);
if (!result.Succeeded)
{
context.Result = new ForbidResult();
}
}
}
class RoomAccessRequirement : EntityAccessRequirementBase
{
public RoomAccessRequirement(string routeParameterName = "id")
: base(routeParameterName) { }
}
class HouseAccessRequirement : EntityAccessRequirementBase
{
public HouseAccessRequirement(string routeParameterName = "id")
: base(routeParameterName) { }
}
class AccountAccessRequirement : EntityAccessRequirementBase
{
public AccountAccessRequirement(string routeParameterName = "id")
: base(routeParameterName) { }
}
class AuthorizeRoomAccessAttribute : TypeFilterAttribute
{
public AuthorizeRoomAccessAttribute(string routeParameterName = "id")
: base(typeof(EntityAccessFilter))
{
Arguments = [new RoomAccessRequirement(routeParameterName)];
}
}
class AuthorizeHouseAccessAttribute : TypeFilterAttribute
{
public AuthorizeHouseAccessAttribute(string routeParameterName = "id")
: base(typeof(EntityAccessFilter))
{
Arguments = [new HouseAccessRequirement(routeParameterName)];
}
}
class AuthorizeAccountAccessAttribute : TypeFilterAttribute
{
public AuthorizeAccountAccessAttribute(string routeParameterName = "id")
: base(typeof(EntityAccessFilter))
{
Arguments = [new AccountAccessRequirement(routeParameterName)];
}
}
class RoomAccessHandler : AuthorizationHandler<RoomAccessRequirement, Guid>
{
private readonly IRoomEntityDbContext _db;
private readonly IUserContext _currentUser;
public RoomAccessHandler(IRoomEntityDbContext db, IUserContext currentUser)
{
_db = db;
_currentUser = currentUser;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RoomAccessRequirement requirement, Guid resourceId)
{
var orgIds = _currentUser.GetUserOrganizationIds(context.User);
var hasAccess = await _db.Rooms
.Where(r => r.Id == resourceId)
.SelectMany(r => r.House.Contracts)
.AnyAsync(c => orgIds.Contains(c.OrganizationId) && (c.RoomId == null || c.RoomId == resourceId));
if (hasAccess)
context.Succeed(requirement);
}
}
class HouseAccessHandler : AuthorizationHandler<HouseAccessRequirement, Guid>
{
private readonly IHouseEntityDbContext _db;
private readonly IUserContext _currentUser;
public HouseAccessHandler(IHouseEntityDbContext db, IUserContext currentUser)
{
_db = db;
_currentUser = currentUser;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, HouseAccessRequirement requirement, Guid resourceId)
{
var orgIds = _currentUser.GetUserOrganizationIds(context.User);
var hasAccess = await _db.Houses
.Where(h => h.Id == resourceId)
.SelectMany(h => h.Contracts)
.AnyAsync(c => orgIds.Contains(c.Organization.Id));
if (hasAccess)
context.Succeed(requirement);
}
}
class AccountAccessHandler : AuthorizationHandler<AccountAccessRequirement, Guid>
{
private readonly IAccountEntityDbContext _db;
private readonly IUserContext _userContext;
public AccountAccessHandler(IAccountEntityDbContext db, IUserContext userContext)
{
_db = db;
_userContext = userContext;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AccountAccessRequirement requirement, Guid accountId)
{
var orgIds = _userContext.GetUserOrganizationIds(context.User);
var isOwner = await _db.Accounts.AnyAsync(a => a.Id == accountId && orgIds.Contains(a.Organization.Id));
if (isOwner)
context.Succeed(requirement);
}
}
[ApiController]
[Route("[controller]")]
public class HousesController : ControllerBase
{
private readonly AppDbContext _db;
public HousesController(AppDbContext db)
{
_db = db;
}
[HttpGet]
[AuthorizeHouseAccess("id")]
public async Task<IActionResult> GetById([FromQuery] Guid id)
{
var house = await _db.Houses.FirstOrDefaultAsync(h => h.Id == id);
if (house is null)
return NotFound();
return Ok(house);
}
[HttpGet("getall")]
[Authorize]
public async Task<IActionResult> GetAccessibleHouses([FromServices] IUserContext userContext)
{
var userOrgIds = userContext.GetUserOrganizationIds(User);
if (User.IsInRole("Admin"))
{
var allHouses = await _db.Houses.ToListAsync();
return Ok(allHouses);
}
var accessibleHouseIds = _db.Contracts.Where(c => userOrgIds.Contains(c.OrganizationId)).Select(c => c.HouseId);
var accessibleHouses = await _db.Houses
.Where(h => accessibleHouseIds.Contains(h.Id))
.ToListAsync();
return Ok(accessibleHouses);
}
}