Задать вопрос
@OwDafuq

Как сделать такую аутентификацию?

Используем: .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);
    }
}
  • Вопрос задан
  • 71 просмотр
Подписаться 1 Простой Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы