Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make authorization to specific field in specific object in ASP.Net Core?

I need to check privileges to specific field in specific object in database.

Let's make and example. I have Model called Employee

public class Employee {

    [Key]
    public int EmployeeID { get; set; }

    public string JobTitle { get; set; }

    public string Description { get; set; }

    public int Salary { get; set; } // <---- Restricted

    public int BossID { get; set; }
}

And I have a few cases:

  1. I need to restrict access to specific field Salary because I don't want anyone to see each other salary. But HR can see anyone Salary and edit it. If I'm this employee I can see my own Salary, but cannot edit it.

  2. Everyone can see each other job titles, but only HR can edit it. And also boss of that employee, can edit, by employee himself cannot.

Use case:

  • I'm manager with RoleID 4. I want to see Salary of my Employee named John Smith with EmployeeID 5. I can do that.

  • I'm manager with RoleID 4. I want to see Salary of 'Employeenamed Mark Twain withEmployeeID` 8. Mark is not but my directly subordinate. He is from different branch. I cannot do that.

  • I'm employee with EmployeeID 5 and I want to see my Salary. That's allowed.

  • I'm employee with EmployeeID 5 and I want to edit my own Salary. It's forbidden. I get HTTP Error 401.

  • I'm from HR. I can see and edit Salary of all Employees in company.

I though of something like this:

public class Access {
  [Required]
  public int RoleID { get; set; }

  [Required]
  public string TableName { get; set; }

  [Required]
  public string ColumnName { get; set; }

  [Required]
  public int RowID { get; set; }
}

And then check (by Authorize attribute) if specific role (boss, HR or something) has access to specific field (for example Salary) for specific data (for example Employee with id 22). That's a lot of "specific"by the way.

How should I do it? Is my idea 'OK'?

like image 368
Morasiu Avatar asked Dec 23 '22 03:12

Morasiu


1 Answers

In case when logic is less complicated or more generic, it's possible to set custom output formatter to prevent some fields to be written into the respose.

The approach has next problems:

  1. Shouldn't handle complicated logic. As it causes business logic spread to the multiple places
  2. Replaces default serialization. So if there are specific serialization settings are set in Startup, then it should be transfered

Let's see an example. There could be a custom attrbute like

public class AuthorizePropertyAttribute : Attribute
{
    public AuthorizePropertyAttribute(string role) => Role = role;
    public string Role { get; set; }
}

Then output formatter could be like:

public class AuthFormatter : TextOutputFormatter
{
    public AuthFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
        SupportedEncodings.Add(Encoding.UTF8);
    }

    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, 
        Encoding selectedEncoding)
    {
        var settings = new JsonSerializerSettings 
        {
            ContractResolver = new AuthorizedPropertyContractResolver(context.HttpContext.User)
        };
        await context.HttpContext.Response.WriteAsync(
            JsonConvert.SerializeObject(context.Object, settings));
    }
}    

That would require

public class AuthorizedPropertyContractResolver : DefaultContractResolver
{
    public AuthorizedPropertyContractResolver(ClaimsPrincipal user)
    {
        User = user;
    }

    public ClaimsPrincipal User { get; }

    protected override JsonProperty CreateProperty(MemberInfo member, 
        MemberSerialization memberSerialization)
    {
        var result = base.CreateProperty(member, memberSerialization);
        result.ShouldSerialize = e =>
        {
            var role = member.GetCustomAttribute<AuthorizePropertyAttribute>()?.Role;
            return string.IsNullOrWhiteSpace(role) ? true : User.IsInRole(role);
        };
        return result;
    }
}

Registration:

services.AddMvc(options =>
{
    options.OutputFormatters.Insert(0, new AuthFormatter());
});

In that case Response for simple user will lack of the Salary field {"Id":1,"Name":"John"} at the same time manager will see the full response {"Id":1,"Name":"John","Salary":100000}, ofcourse the property "Salary" should have attribute set

[AuthorizeProperty("Boss")]
public double Salary { get; set; }
like image 137
ASpirin Avatar answered Mar 02 '23 00:03

ASpirin