Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Claims #13

Closed
papyr opened this issue May 11, 2020 · 5 comments
Closed

Dynamic Claims #13

papyr opened this issue May 11, 2020 · 5 comments

Comments

@papyr
Copy link

papyr commented May 11, 2020

Hello, can you share some suggestion on how to do secure data in the same role across groups, I was thinking claims might be good.

For e.g. when you have managers in the same role, but you want to limit the data to only their geographic areas so they don't see other financial data.

Thanks

@mo-esmp
Copy link
Owner

mo-esmp commented May 11, 2020

I have implemented this scenario several years ago. You can use roles or claims but I used claims. Here are some sample codes and they might help you. These codes are related to the EF Core 1.0 and now with EF Core 3, you may find a more elegant way to filter data.

Imagine I have two roles Representative and Agent I want to apply a filter on specific entities:

public static class DataAccessExtensions
{
    public static IQueryable<TEntity> ApplyGeoSecurityFilter<TEntity>(this IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor) where TEntity : BaseEntity
    {
#if DEBUG
        if (httpContextAccessor == null)
            return query;
#endif
        var userType = httpContextAccessor.HttpContext.User.Identity.Get<UserType>();

        if (userType == UserType.Representative && (typeof(TEntity) == typeof(CustomerEntity)))
            query = new GeographyDataFilterForAgent().ApplyFilter(query, context, httpContextAccessor);

        return query;
    }

    public static IQueryable<TEntity> ApplySecurityFilter<TEntity>(this IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
        where TEntity : BaseEntity
    {
#if DEBUG
        if (httpContextAccessor == null)
            return query;
#endif
        var userType = httpContextAccessor.HttpContext.User.Identity.Get<UserType>();
        switch (userType)
        {
            case UserType.System:
            case UserType.SuperAdmin:
                break;

            case UserType.Representative:
                query = new DataFilterForRepresentative().ApplyFilter(query, context, httpContextAccessor);
                break;

            case UserType.Customer:
                query = new DataFilterForCustomer().ApplyFilter(query, context, httpContextAccessor);
                break;

            default:
                throw new ArgumentOutOfRangeException();
        }

        return query;
    }
}

And here are filters for each role:

internal class GeographyDataFilterForAgent : BaseDataFilter
{
    public override IQueryable<TEntity> ApplyFilter<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
    {
        return ApplyGeoFilter(query, context, httpContextAccessor);
    }

    private static IQueryable<TEntity> ApplyGeoFilter<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
        where TEntity : BaseEntity
    {
        var userName = httpContextAccessor.HttpContext.User.Identity.Name;
        var geoQuery = GetGeographyQuery(context, userName);

        query = (query as IQueryable<CustomerEntity>).Where(c => geoQuery.Any(g =>
            (g.GeographyType == GeographyType.City && g.Id == c.Contact.CityId) ||
            (g.GeographyType == GeographyType.Province && g.Id == c.Contact.ProvinceId))) as IQueryable<TEntity>;

        return query;
    }
}

/// <summary>
/// Data filter for representative.
/// </summary>
/// <seealso cref="BaseDataFilter" />
internal class DataFilterForRepresentative : BaseDataFilter
{
    /// <summary>
    /// Applies the security filter.
    /// </summary>
    /// <typeparam name="TEntity">The type of the t entity.</typeparam>
    /// <param name="query">The query.</param>
    /// <param name="context">The context.</param>
    /// <param name="httpContextAccessor">The HTTP context accessor.</param>
    /// <returns>IQueryable&lt;TEntity&gt;.</returns>
    public override IQueryable<TEntity> ApplyFilter<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
    {
        var entityType = typeof(TEntity);
        
        // There was no pattern matching at that time
        if (entityType == typeof(CustomerEntity))
            query = ApplyFilterOnCustomerForAgent(query, context, httpContextAccessor);

        // and want fetch CustomerServiceEntity
        else if (entityType == typeof(CustomerServiceEntity))
            query = ApplyFilterOnServiceForAgent(query, context, httpContextAccessor);

        // and want fetch OrderEntity
        else if (entityType == typeof(OrderEntity))
            query = ApplyFilterOnContractForAgent(query, context, httpContextAccessor);

        // and want fetch InvoiceEntity
        else if (entityType == typeof(InvoiceEntity))
            query = ApplyFilterOnInvoiceForAgent(query, context, httpContextAccessor);

        return query;
    }

    /// <summary>
    /// Applies the filter on contract .
    /// </summary>
    /// <typeparam name="TEntity">The type of the t entity.</typeparam>
    /// <param name="query">The query.</param>
    /// <param name="context">The context.</param>
    /// <param name="httpContextAccessor">The HTTP context accessor.</param>
    /// <returns>IQueryable&lt;TEntity&gt;.</returns>
    private static IQueryable<TEntity> ApplyFilterOnContractForAgent<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
        where TEntity : BaseEntity
    {
        var userId = httpContextAccessor.HttpContext.User.Identity.GetUserId();

        Expression<Func<OrderEntity, bool>> filter = o => o.OrderItems.Any(oi =>
                       oi.ServiceOrderItem.CustomerService.RepresentativeId ==
                         context.RepresentativeEmployees.FirstOrDefault(e => e.UserId == userId).RepresentativeId);

        return (query as IQueryable<OrderEntity>).Where(filter) as IQueryable<TEntity>;
    }

    /// <summary>
    /// Applies the filter on contract request .
    /// </summary>
    /// <typeparam name="TEntity">The type of the t entity.</typeparam>
    /// <param name="query">The query.</param>
    /// <param name="context">The context.</param>
    /// <param name="httpContextAccessor">The HTTP context accessor.</param>
    /// <returns>IQueryable&lt;TEntity&gt;.</returns>
    private static IQueryable<TEntity> ApplyFilterOnContractRequestForAgent<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
       where TEntity : BaseEntity
    {
        var userId = httpContextAccessor.HttpContext.User.Identity.GetUserId();

        Expression<Func<OrderRequestEntity, bool>> filter = o => o.Order.OrderItems.Any(oi =>
                        oi.ServiceOrderItem.CustomerService.RepresentativeId ==
                            context.RepresentativeEmployees.FirstOrDefault(e => e.UserId == userId).RepresentativeId);

        return (query as IQueryable<OrderRequestEntity>).Where(filter) as IQueryable<TEntity>;
    }

    /// <summary>
    /// Applies the filter on customer.
    /// </summary>
    /// <typeparam name="TEntity">The type of the t entity.</typeparam>
    /// <param name="query">The query.</param>
    /// <param name="context">The context.</param>
    /// <param name="httpContextAccessor">The HTTP context accessor.</param>
    /// <returns>IQueryable&lt;TEntity&gt;.</returns>
    private static IQueryable<TEntity> ApplyFilterOnCustomerForAgent<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
        where TEntity : BaseEntity
    {
        var userName = httpContextAccessor.HttpContext.User.Identity.Name;
        var representativeQuery = GetServiceRepresentativeQuery(context, userName);

        return (query as IQueryable<CustomerEntity>).Where(c => c.Services.Any(s => representativeQuery.Any(rs => rs.Id == s.Id))) as IQueryable<TEntity>;
    }

    /// <summary>
    /// Applies the filter on invoice .
    /// </summary>
    /// <typeparam name="TEntity">The type of the t entity.</typeparam>
    /// <param name="query">The query.</param>
    /// <param name="context">The context.</param>
    /// <param name="httpContextAccessor">The HTTP context accessor.</param>
    /// <returns>IQueryable&lt;TEntity&gt;.</returns>
    private static IQueryable<TEntity> ApplyFilterOnInvoiceForAgent<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
        where TEntity : BaseEntity
    {
        var userName = httpContextAccessor.HttpContext.User.Identity.Name;
        var representativeQuery = GetServiceRepresentativeQuery(context, userName);

        return (query as IQueryable<InvoiceEntity>).Where(c => c.Customer.Services.Any(s => representativeQuery.Any(rs => rs.Id == s.Id))) as IQueryable<TEntity>;
    }
}

internal abstract class BaseDataFilter
{
    /// <summary>
    /// Applies the security filter.
    /// </summary>
    /// <typeparam name="TEntity">The type of the t entity.</typeparam>
    /// <param name="query">The query.</param>
    /// <param name="context">The context.</param>
    /// <param name="httpContextAccessor">The HTTP context accessor.</param>
    /// <returns>IQueryable&lt;TEntity&gt;.</returns>
    public abstract IQueryable<TEntity> ApplyFilter<TEntity>(IQueryable<TEntity> query, DataContext context, IHttpContextAccessor httpContextAccessor)
        where TEntity : BaseEntity;

    /// <summary>
    /// Gets the representative services query by covered geography zones.
    /// </summary>
    /// <param name="context">The context.</param>
    /// <param name="userName">Name of the user.</param>
    /// <returns>IQueryable&lt;GeographyEntity&gt;.</returns>
    protected static IQueryable<GeographyEntity> GetGeographyQuery(DataContext context, string userName)
    {
        return from user in context.Users
               join representativeEmployee in context.RepresentativeEmployees on user.Id equals representativeEmployee.UserId.Value
               join representative in context.Representatives on representativeEmployee.RepresentativeId equals representative.Id
               join representativeCoveredZone in context.RepresentativeCoveredZones on representative.Id equals representativeCoveredZone.RepresentativeId
               join geography in context.Geographies on representativeCoveredZone.GeographyId equals geography.Id
               where user.UserName == userName
               select geography;
    }

    /// <summary>
    /// Gets the representative services query..
    /// </summary>
    /// <param name="context">The context.</param>
    /// <param name="userName">Name of the user.</param>
    /// <returns>IQueryable&lt;CustomerServiceRepresentativeEntity&gt;.</returns>
    protected static IQueryable<CustomerServiceEntity> GetServiceRepresentativeQuery(DataContext context, string userName)
    {
        return from user in context.Users
               join representativeEmployee in context.RepresentativeEmployees on user.Id equals representativeEmployee.UserId.Value
               join representative in context.Representatives on representativeEmployee.RepresentativeId equals representative.Id
               join service in context.CustomerServices on representative.Id equals service.RepresentativeId
               where user.UserName == userName
               select service;
    }
}

It's time to apply a filter:

public class CustomerRepository: ICustomerRepository
{
    private readonly DataContext _context;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CustomerQueryExtensions(DataContext context)
    {
        _context = context;
    }

    public Task<CustomerEntity> GetCustomerByIdAsync(int customerId)
    {
        return _context.Customers.ApplySecurityFilter(_context, _httpContextAccessor)
            .SingleOrDefaultAsync(m => m.Id == customerId && !m.IsDeleted);
    }

@mo-esmp mo-esmp pinned this issue May 11, 2020
@mo-esmp
Copy link
Owner

mo-esmp commented Jun 20, 2020

I close this issue due to inactivity. Feel free to reopen it.

@mo-esmp mo-esmp closed this as completed Jun 20, 2020
@papyr
Copy link
Author

papyr commented Oct 8, 2020

Hello I like you filter concept its very good, I have been trying to get it working and I got stuck on how to allow the user to set it.

I saw you are using CityId on the customer, but I need to do it on the claims side allowing Super Admin to set it inside the UI. so User can add the values to allow the managers to be filtered by base filters, so I used claims. Can you please tell me if this is correct.

Here is what I tried, I added role-claims to your code. Since it dynamic, I was not sure if this is correct.

Use Case: we need Super-Admin Users inside UI to create groups for data access inside a role. And, right now I have role for manager to limit the managers data access to salaries, budgets etc. based on their area like state or city or region.

So EastCoast-Managers, WestCoast-Managers etc. can only see salaries in their areas. I am trying the code below, but I don't know how to create those groups like in Active Directory

public async Task<ActionResult> UpdateRole(string id, string name, List<KeyValuePair<string, string>> claims)
        {
            try
            {
                var role = await _roleManager.FindByIdAsync(id);
                if (role == null)
                    return NotFound("mo esmp for Role not found.");

                role.Name = name;

                var result = await _roleManager.UpdateAsync(role);
                if (result.Succeeded)
                {
                    _logger.LogInformation("Updated role {name}.", role.Name);

                    var roleClaims = await _roleManager.GetClaimsAsync(role);

                    foreach (var kvp in claims.Where(a => !roleClaims.Any(b => _claimTypes[a.Key] == b.Type && a.Value == b.Value)))
                        await _roleManager.AddClaimAsync(role, new Claim(_claimTypes[kvp.Key], kvp.Value));

                    foreach (var claim in roleClaims.Where(a => !claims.Any(b => a.Type == _claimTypes[b.Key] && a.Value == b.Value)))
                        await _roleManager.RemoveClaimAsync(role, claim);

                    return NoContent();
                }
                else
                    return BadRequest(result.Errors.First().Description);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failure updating role {roleId}.", id);
                return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
            }
        }

@mo-esmp
Copy link
Owner

mo-esmp commented Oct 9, 2020

Your codes look fine and perhaps for the next version I should use claims to store role access. I have no experience with active directory and I couldn't help you with that, sorry.

@papyr
Copy link
Author

papyr commented Oct 12, 2020

Ohh no, Not looking for AD!

Just that groups are a common functionality inside roles, something that AD had. I just don't know how to go about this so I am playing around and watching for your input. And, for ref. here are a couple of over blown code samples

In summary, just a minimal function of groups within roles so that we can secure-isolate-data for people based on an area-location, or language etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants