Closed papyr closed 4 years ago
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<TEntity>.</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<TEntity>.</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<TEntity>.</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<TEntity>.</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<TEntity>.</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<TEntity>.</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<GeographyEntity>.</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<CustomerServiceRepresentativeEntity>.</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);
}
I close this issue due to inactivity. Feel free to reopen it.
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);
}
}
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.
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.
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