.Net Core Api - Custom JSON Resolver based on Request Values
I'm looking to have all OkObjectResult
responses coming out of my api get run through a custom JSON resolver that I have. The resolver relies on some request-specific data - namely, the user's roles. It's effectively like the Authorize attribute on a controller, but for data transfer objects passed from the API to the UI.
I can add the resolver in Configure Services via AddJsonOptions, but it doesn't have access to that user info there.
How can I pass values that are based on the request to this resolver? Am I looking at some sort of custom middleware, or something else?
As a sample, if I have an object with some custom attribute decorators, like so:
public class TestObject
{
public String Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public String Field2 => "ViewRequiresAdmin";
}
And call my custom serializer with different roles, like so:
var test = new TestObject();
var userRoles = GetRoles(); // "User" for the sake of this example
var outputJson = JsonConvert.SerializeObject(test,
new JsonSerializerSettings {
ContractResolver = new MyCustomResolver(userRoles)
});
Then the output JSON will skip anything the user can't access, like so:
{
"Field1":"NoRestrictions",
// Note the absence of Field2, since it has [RequireRoleView("Admin")]
}
c# json.net asp.net-core-webapi
add a comment |
I'm looking to have all OkObjectResult
responses coming out of my api get run through a custom JSON resolver that I have. The resolver relies on some request-specific data - namely, the user's roles. It's effectively like the Authorize attribute on a controller, but for data transfer objects passed from the API to the UI.
I can add the resolver in Configure Services via AddJsonOptions, but it doesn't have access to that user info there.
How can I pass values that are based on the request to this resolver? Am I looking at some sort of custom middleware, or something else?
As a sample, if I have an object with some custom attribute decorators, like so:
public class TestObject
{
public String Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public String Field2 => "ViewRequiresAdmin";
}
And call my custom serializer with different roles, like so:
var test = new TestObject();
var userRoles = GetRoles(); // "User" for the sake of this example
var outputJson = JsonConvert.SerializeObject(test,
new JsonSerializerSettings {
ContractResolver = new MyCustomResolver(userRoles)
});
Then the output JSON will skip anything the user can't access, like so:
{
"Field1":"NoRestrictions",
// Note the absence of Field2, since it has [RequireRoleView("Admin")]
}
c# json.net asp.net-core-webapi
2
Can you pass theuserRoles
intoTestObject
when it is constructed? If all your objects that haveRequireRoleViewAttribute
applied could haveuserRoles
as an internal property, you could make a custom contract resolver that does the necessary checks in a customJsonProperty.ShouldSerialize
predicate.
– dbc
Nov 13 '18 at 20:28
1
Yeah, my hesitance on ShouldSerialize is in potential for name changes and stuff like that in the future - changing a property meaning we need to remember to change ShouldSerialize, etc. I can probably updateMyCustomResolver
to check for that property on the object being serialized though, instead of needing the roles passed to it when called. I'll poke around with that and see how it goes. Thanks!
– user1874135
Nov 13 '18 at 20:39
2
Just to be clear, I meant that the custom contract resolver itself could add a syntheticShouldSerialize
predicate based on the current user roles and value forRequireRoleView
. I.e. the logic would be similar to the contract resolver from this answer but rather than doing the checks directly inCreateProperty
,CreateProperty()
would add aShouldSerialize
predicate that would check against object's roles.
– dbc
Nov 13 '18 at 20:46
add a comment |
I'm looking to have all OkObjectResult
responses coming out of my api get run through a custom JSON resolver that I have. The resolver relies on some request-specific data - namely, the user's roles. It's effectively like the Authorize attribute on a controller, but for data transfer objects passed from the API to the UI.
I can add the resolver in Configure Services via AddJsonOptions, but it doesn't have access to that user info there.
How can I pass values that are based on the request to this resolver? Am I looking at some sort of custom middleware, or something else?
As a sample, if I have an object with some custom attribute decorators, like so:
public class TestObject
{
public String Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public String Field2 => "ViewRequiresAdmin";
}
And call my custom serializer with different roles, like so:
var test = new TestObject();
var userRoles = GetRoles(); // "User" for the sake of this example
var outputJson = JsonConvert.SerializeObject(test,
new JsonSerializerSettings {
ContractResolver = new MyCustomResolver(userRoles)
});
Then the output JSON will skip anything the user can't access, like so:
{
"Field1":"NoRestrictions",
// Note the absence of Field2, since it has [RequireRoleView("Admin")]
}
c# json.net asp.net-core-webapi
I'm looking to have all OkObjectResult
responses coming out of my api get run through a custom JSON resolver that I have. The resolver relies on some request-specific data - namely, the user's roles. It's effectively like the Authorize attribute on a controller, but for data transfer objects passed from the API to the UI.
I can add the resolver in Configure Services via AddJsonOptions, but it doesn't have access to that user info there.
How can I pass values that are based on the request to this resolver? Am I looking at some sort of custom middleware, or something else?
As a sample, if I have an object with some custom attribute decorators, like so:
public class TestObject
{
public String Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public String Field2 => "ViewRequiresAdmin";
}
And call my custom serializer with different roles, like so:
var test = new TestObject();
var userRoles = GetRoles(); // "User" for the sake of this example
var outputJson = JsonConvert.SerializeObject(test,
new JsonSerializerSettings {
ContractResolver = new MyCustomResolver(userRoles)
});
Then the output JSON will skip anything the user can't access, like so:
{
"Field1":"NoRestrictions",
// Note the absence of Field2, since it has [RequireRoleView("Admin")]
}
c# json.net asp.net-core-webapi
c# json.net asp.net-core-webapi
asked Nov 13 '18 at 20:01
user1874135user1874135
8712
8712
2
Can you pass theuserRoles
intoTestObject
when it is constructed? If all your objects that haveRequireRoleViewAttribute
applied could haveuserRoles
as an internal property, you could make a custom contract resolver that does the necessary checks in a customJsonProperty.ShouldSerialize
predicate.
– dbc
Nov 13 '18 at 20:28
1
Yeah, my hesitance on ShouldSerialize is in potential for name changes and stuff like that in the future - changing a property meaning we need to remember to change ShouldSerialize, etc. I can probably updateMyCustomResolver
to check for that property on the object being serialized though, instead of needing the roles passed to it when called. I'll poke around with that and see how it goes. Thanks!
– user1874135
Nov 13 '18 at 20:39
2
Just to be clear, I meant that the custom contract resolver itself could add a syntheticShouldSerialize
predicate based on the current user roles and value forRequireRoleView
. I.e. the logic would be similar to the contract resolver from this answer but rather than doing the checks directly inCreateProperty
,CreateProperty()
would add aShouldSerialize
predicate that would check against object's roles.
– dbc
Nov 13 '18 at 20:46
add a comment |
2
Can you pass theuserRoles
intoTestObject
when it is constructed? If all your objects that haveRequireRoleViewAttribute
applied could haveuserRoles
as an internal property, you could make a custom contract resolver that does the necessary checks in a customJsonProperty.ShouldSerialize
predicate.
– dbc
Nov 13 '18 at 20:28
1
Yeah, my hesitance on ShouldSerialize is in potential for name changes and stuff like that in the future - changing a property meaning we need to remember to change ShouldSerialize, etc. I can probably updateMyCustomResolver
to check for that property on the object being serialized though, instead of needing the roles passed to it when called. I'll poke around with that and see how it goes. Thanks!
– user1874135
Nov 13 '18 at 20:39
2
Just to be clear, I meant that the custom contract resolver itself could add a syntheticShouldSerialize
predicate based on the current user roles and value forRequireRoleView
. I.e. the logic would be similar to the contract resolver from this answer but rather than doing the checks directly inCreateProperty
,CreateProperty()
would add aShouldSerialize
predicate that would check against object's roles.
– dbc
Nov 13 '18 at 20:46
2
2
Can you pass the
userRoles
into TestObject
when it is constructed? If all your objects that have RequireRoleViewAttribute
applied could have userRoles
as an internal property, you could make a custom contract resolver that does the necessary checks in a custom JsonProperty.ShouldSerialize
predicate.– dbc
Nov 13 '18 at 20:28
Can you pass the
userRoles
into TestObject
when it is constructed? If all your objects that have RequireRoleViewAttribute
applied could have userRoles
as an internal property, you could make a custom contract resolver that does the necessary checks in a custom JsonProperty.ShouldSerialize
predicate.– dbc
Nov 13 '18 at 20:28
1
1
Yeah, my hesitance on ShouldSerialize is in potential for name changes and stuff like that in the future - changing a property meaning we need to remember to change ShouldSerialize, etc. I can probably update
MyCustomResolver
to check for that property on the object being serialized though, instead of needing the roles passed to it when called. I'll poke around with that and see how it goes. Thanks!– user1874135
Nov 13 '18 at 20:39
Yeah, my hesitance on ShouldSerialize is in potential for name changes and stuff like that in the future - changing a property meaning we need to remember to change ShouldSerialize, etc. I can probably update
MyCustomResolver
to check for that property on the object being serialized though, instead of needing the roles passed to it when called. I'll poke around with that and see how it goes. Thanks!– user1874135
Nov 13 '18 at 20:39
2
2
Just to be clear, I meant that the custom contract resolver itself could add a synthetic
ShouldSerialize
predicate based on the current user roles and value for RequireRoleView
. I.e. the logic would be similar to the contract resolver from this answer but rather than doing the checks directly in CreateProperty
, CreateProperty()
would add a ShouldSerialize
predicate that would check against object's roles.– dbc
Nov 13 '18 at 20:46
Just to be clear, I meant that the custom contract resolver itself could add a synthetic
ShouldSerialize
predicate based on the current user roles and value for RequireRoleView
. I.e. the logic would be similar to the contract resolver from this answer but rather than doing the checks directly in CreateProperty
, CreateProperty()
would add a ShouldSerialize
predicate that would check against object's roles.– dbc
Nov 13 '18 at 20:46
add a comment |
2 Answers
2
active
oldest
votes
Suppose you have an custom RequireRoleViewAttribute
:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
public string Role;
public RequireRoleViewAttribute(string role){
this.Role = role;
}
}
How can I pass values that are based on the request to this resolver?
You can have a IServiceProvider
injected in your custom resolver :
public class RoleBasedContractResolver : DefaultContractResolver
{
public IServiceProvider ServiceProvider { get; }
public RoleBasedContractResolver( IServiceProvider sp)
{
this.ServiceProvider = sp;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you're using the Identity, you can get the userManager :
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
// ...
}
}
thus we can get the HttpContext
and User
as we like. If you're using the Identity, you can also get the UserManager
service and roles.
and now we can follow @dbc's advice to control the ShouldSerialize
:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you use the Identitiy, you can get the usermanager
//UserManager<IdentityUser>
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
JsonProperty property = base.CreateProperty(member, memberSerialization);
// get the attributes
var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
// if no [RequireResoveView] decorated, always serialize it
if(attrs.Count()==0) {
property.ShouldDeserialize = instance => true;
return property;
}
// custom your logic to dertermine wether should serialize the property
// I just use check if it can statisify any the condition :
var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
property.ShouldSerialize = instance => {
var resource = new { /* any you need */ };
return attrs.Any(attr => {
var rolename = attr.Role;
return roles.Any(r => r == rolename ) ;
}) ? true : false;
};
return property;
}
The function GetIdentityUserRolesAsync
here is helper method to retrieve roles using the current HttpContext
and the UserManger
service :
private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
var rolesCached= context.Items["__userRoles__"];
if( rolesCached != null){
return (IList<string>) rolesCached;
}
var identityUser = await userManager.GetUserAsync(context.User);
var roles = await userManager.GetRolesAsync(identityUser);
context.Items["__userRoles__"] = roles;
return roles;
}
How to inject the IServiceProvider
in details :
The trick is all about how to configure the default MvcJwtOptions
with an IServiceProvider
.
Don't configure the JsonOptions
by :
services.AddMvc().
.AddJsonOptions(o =>{
// o.
});
as it doesn't allow us add a IServiceProvider
parameter.
We can custom a subclass of MvcJsonOptions
:
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}
and register the services by :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// don't forget to add the IHttpContextAccessor
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
Test Case :
Let's say you have a custom POCO :
public class TestObject
{
public string Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public string Field2 => "ViewRequiresAdmin";
[RequireRoleView("HR"),RequireRoleView("OP")]
public string Field3 => "ViewRequiresHROrOP";
[RequireRoleView("IT"), RequireRoleView("HR")]
public string Field4 => "ViewRequiresITOrHR";
[RequireRoleView("IT"), RequireRoleView("OP")]
public string Field5 => "ViewRequiresITOrOP";
}
And the Current User has roles : Admin
and HR
:
The result will be :
{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}
A screenshot of testing with an action method :
1
Yaaaay, it works! I was having some issues with using UserManager.GetRolesAsync (service not registered, etc.), so I wound up falling back to getting user roles by parsing the claims, but otherwise it's pretty slick. I also set up some extension methods, so using it is just a matter of adding theRequireRoleView
attribute and callingservices.AddRoleBasedContractResolver()
inStartup.ConfigureServices
. Thanks to both of you :) I've accepted yours as the answer and I'll post my own with my tweaks in case anyone in the future finds it useful.
– user1874135
Nov 14 '18 at 15:53
add a comment |
Itminus's answer covers everything that's needed, but for anyone interested, I've extended it a little for easy reuse.
First, in a class library
My RequireRoleViewAttribute, which allows multiple roles (OR, not AND):
[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
public List<String> AllowedRoles { get; set; }
public RequireRoleViewAttribute(params String AllowedRoles) =>
this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}
My resolver is almost identical to Itminus's, but CreateProperty is adjusted to
IEnumerable<String> userRoles = this.GetIdentityUserRoles();
property.ShouldSerialize = instance =>
{
// Check if every attribute instance has at least one role listed in the user's roles.
return attrs.All(attr =>
userRoles.Any(ur =>
attr.AllowedRoles.Any(ar =>
String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
);
};
And GetIdentityUserRoles doesn't use UserManager
private IEnumerable<String> GetIdentityUserRoles()
{
IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext context = contextAccessor.HttpContext;
ClaimsPrincipal user = context.User;
Object rolesCached = context.Items["__userRoles__"];
if (rolesCached != null)
{
return (List<String>)rolesCached;
}
var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
context.Items["__userRoles__"] = roles;
return roles;
}
And I have an extensions class which contains:
public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
return services;
}
Then in my API
I reference that class library. In Startup.cs -> ConfigureServices, I call:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddRoleBasedContractResolver();
...
}
And my DTOs are tagged with the attribute:
public class Diagnostics
{
public String VersionNumber { get; set; }
[RequireRoleView("admin")]
public Boolean ViewIfAdmin => true;
[RequireRoleView("hr")]
public Boolean ViewIfHr => true;
[RequireRoleView("hr", "admin")]
public Boolean ViewIfHrOrAdmin => true;
}
And the return value as an admin is:
{
"VersionNumber": "Debug",
"ViewIfAdmin": true,
"ViewIfHrOrAdmin": true
}
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53288633%2fnet-core-api-custom-json-resolver-based-on-request-values%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
Suppose you have an custom RequireRoleViewAttribute
:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
public string Role;
public RequireRoleViewAttribute(string role){
this.Role = role;
}
}
How can I pass values that are based on the request to this resolver?
You can have a IServiceProvider
injected in your custom resolver :
public class RoleBasedContractResolver : DefaultContractResolver
{
public IServiceProvider ServiceProvider { get; }
public RoleBasedContractResolver( IServiceProvider sp)
{
this.ServiceProvider = sp;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you're using the Identity, you can get the userManager :
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
// ...
}
}
thus we can get the HttpContext
and User
as we like. If you're using the Identity, you can also get the UserManager
service and roles.
and now we can follow @dbc's advice to control the ShouldSerialize
:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you use the Identitiy, you can get the usermanager
//UserManager<IdentityUser>
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
JsonProperty property = base.CreateProperty(member, memberSerialization);
// get the attributes
var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
// if no [RequireResoveView] decorated, always serialize it
if(attrs.Count()==0) {
property.ShouldDeserialize = instance => true;
return property;
}
// custom your logic to dertermine wether should serialize the property
// I just use check if it can statisify any the condition :
var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
property.ShouldSerialize = instance => {
var resource = new { /* any you need */ };
return attrs.Any(attr => {
var rolename = attr.Role;
return roles.Any(r => r == rolename ) ;
}) ? true : false;
};
return property;
}
The function GetIdentityUserRolesAsync
here is helper method to retrieve roles using the current HttpContext
and the UserManger
service :
private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
var rolesCached= context.Items["__userRoles__"];
if( rolesCached != null){
return (IList<string>) rolesCached;
}
var identityUser = await userManager.GetUserAsync(context.User);
var roles = await userManager.GetRolesAsync(identityUser);
context.Items["__userRoles__"] = roles;
return roles;
}
How to inject the IServiceProvider
in details :
The trick is all about how to configure the default MvcJwtOptions
with an IServiceProvider
.
Don't configure the JsonOptions
by :
services.AddMvc().
.AddJsonOptions(o =>{
// o.
});
as it doesn't allow us add a IServiceProvider
parameter.
We can custom a subclass of MvcJsonOptions
:
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}
and register the services by :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// don't forget to add the IHttpContextAccessor
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
Test Case :
Let's say you have a custom POCO :
public class TestObject
{
public string Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public string Field2 => "ViewRequiresAdmin";
[RequireRoleView("HR"),RequireRoleView("OP")]
public string Field3 => "ViewRequiresHROrOP";
[RequireRoleView("IT"), RequireRoleView("HR")]
public string Field4 => "ViewRequiresITOrHR";
[RequireRoleView("IT"), RequireRoleView("OP")]
public string Field5 => "ViewRequiresITOrOP";
}
And the Current User has roles : Admin
and HR
:
The result will be :
{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}
A screenshot of testing with an action method :
1
Yaaaay, it works! I was having some issues with using UserManager.GetRolesAsync (service not registered, etc.), so I wound up falling back to getting user roles by parsing the claims, but otherwise it's pretty slick. I also set up some extension methods, so using it is just a matter of adding theRequireRoleView
attribute and callingservices.AddRoleBasedContractResolver()
inStartup.ConfigureServices
. Thanks to both of you :) I've accepted yours as the answer and I'll post my own with my tweaks in case anyone in the future finds it useful.
– user1874135
Nov 14 '18 at 15:53
add a comment |
Suppose you have an custom RequireRoleViewAttribute
:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
public string Role;
public RequireRoleViewAttribute(string role){
this.Role = role;
}
}
How can I pass values that are based on the request to this resolver?
You can have a IServiceProvider
injected in your custom resolver :
public class RoleBasedContractResolver : DefaultContractResolver
{
public IServiceProvider ServiceProvider { get; }
public RoleBasedContractResolver( IServiceProvider sp)
{
this.ServiceProvider = sp;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you're using the Identity, you can get the userManager :
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
// ...
}
}
thus we can get the HttpContext
and User
as we like. If you're using the Identity, you can also get the UserManager
service and roles.
and now we can follow @dbc's advice to control the ShouldSerialize
:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you use the Identitiy, you can get the usermanager
//UserManager<IdentityUser>
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
JsonProperty property = base.CreateProperty(member, memberSerialization);
// get the attributes
var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
// if no [RequireResoveView] decorated, always serialize it
if(attrs.Count()==0) {
property.ShouldDeserialize = instance => true;
return property;
}
// custom your logic to dertermine wether should serialize the property
// I just use check if it can statisify any the condition :
var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
property.ShouldSerialize = instance => {
var resource = new { /* any you need */ };
return attrs.Any(attr => {
var rolename = attr.Role;
return roles.Any(r => r == rolename ) ;
}) ? true : false;
};
return property;
}
The function GetIdentityUserRolesAsync
here is helper method to retrieve roles using the current HttpContext
and the UserManger
service :
private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
var rolesCached= context.Items["__userRoles__"];
if( rolesCached != null){
return (IList<string>) rolesCached;
}
var identityUser = await userManager.GetUserAsync(context.User);
var roles = await userManager.GetRolesAsync(identityUser);
context.Items["__userRoles__"] = roles;
return roles;
}
How to inject the IServiceProvider
in details :
The trick is all about how to configure the default MvcJwtOptions
with an IServiceProvider
.
Don't configure the JsonOptions
by :
services.AddMvc().
.AddJsonOptions(o =>{
// o.
});
as it doesn't allow us add a IServiceProvider
parameter.
We can custom a subclass of MvcJsonOptions
:
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}
and register the services by :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// don't forget to add the IHttpContextAccessor
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
Test Case :
Let's say you have a custom POCO :
public class TestObject
{
public string Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public string Field2 => "ViewRequiresAdmin";
[RequireRoleView("HR"),RequireRoleView("OP")]
public string Field3 => "ViewRequiresHROrOP";
[RequireRoleView("IT"), RequireRoleView("HR")]
public string Field4 => "ViewRequiresITOrHR";
[RequireRoleView("IT"), RequireRoleView("OP")]
public string Field5 => "ViewRequiresITOrOP";
}
And the Current User has roles : Admin
and HR
:
The result will be :
{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}
A screenshot of testing with an action method :
1
Yaaaay, it works! I was having some issues with using UserManager.GetRolesAsync (service not registered, etc.), so I wound up falling back to getting user roles by parsing the claims, but otherwise it's pretty slick. I also set up some extension methods, so using it is just a matter of adding theRequireRoleView
attribute and callingservices.AddRoleBasedContractResolver()
inStartup.ConfigureServices
. Thanks to both of you :) I've accepted yours as the answer and I'll post my own with my tweaks in case anyone in the future finds it useful.
– user1874135
Nov 14 '18 at 15:53
add a comment |
Suppose you have an custom RequireRoleViewAttribute
:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
public string Role;
public RequireRoleViewAttribute(string role){
this.Role = role;
}
}
How can I pass values that are based on the request to this resolver?
You can have a IServiceProvider
injected in your custom resolver :
public class RoleBasedContractResolver : DefaultContractResolver
{
public IServiceProvider ServiceProvider { get; }
public RoleBasedContractResolver( IServiceProvider sp)
{
this.ServiceProvider = sp;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you're using the Identity, you can get the userManager :
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
// ...
}
}
thus we can get the HttpContext
and User
as we like. If you're using the Identity, you can also get the UserManager
service and roles.
and now we can follow @dbc's advice to control the ShouldSerialize
:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you use the Identitiy, you can get the usermanager
//UserManager<IdentityUser>
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
JsonProperty property = base.CreateProperty(member, memberSerialization);
// get the attributes
var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
// if no [RequireResoveView] decorated, always serialize it
if(attrs.Count()==0) {
property.ShouldDeserialize = instance => true;
return property;
}
// custom your logic to dertermine wether should serialize the property
// I just use check if it can statisify any the condition :
var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
property.ShouldSerialize = instance => {
var resource = new { /* any you need */ };
return attrs.Any(attr => {
var rolename = attr.Role;
return roles.Any(r => r == rolename ) ;
}) ? true : false;
};
return property;
}
The function GetIdentityUserRolesAsync
here is helper method to retrieve roles using the current HttpContext
and the UserManger
service :
private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
var rolesCached= context.Items["__userRoles__"];
if( rolesCached != null){
return (IList<string>) rolesCached;
}
var identityUser = await userManager.GetUserAsync(context.User);
var roles = await userManager.GetRolesAsync(identityUser);
context.Items["__userRoles__"] = roles;
return roles;
}
How to inject the IServiceProvider
in details :
The trick is all about how to configure the default MvcJwtOptions
with an IServiceProvider
.
Don't configure the JsonOptions
by :
services.AddMvc().
.AddJsonOptions(o =>{
// o.
});
as it doesn't allow us add a IServiceProvider
parameter.
We can custom a subclass of MvcJsonOptions
:
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}
and register the services by :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// don't forget to add the IHttpContextAccessor
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
Test Case :
Let's say you have a custom POCO :
public class TestObject
{
public string Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public string Field2 => "ViewRequiresAdmin";
[RequireRoleView("HR"),RequireRoleView("OP")]
public string Field3 => "ViewRequiresHROrOP";
[RequireRoleView("IT"), RequireRoleView("HR")]
public string Field4 => "ViewRequiresITOrHR";
[RequireRoleView("IT"), RequireRoleView("OP")]
public string Field5 => "ViewRequiresITOrOP";
}
And the Current User has roles : Admin
and HR
:
The result will be :
{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}
A screenshot of testing with an action method :
Suppose you have an custom RequireRoleViewAttribute
:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
public string Role;
public RequireRoleViewAttribute(string role){
this.Role = role;
}
}
How can I pass values that are based on the request to this resolver?
You can have a IServiceProvider
injected in your custom resolver :
public class RoleBasedContractResolver : DefaultContractResolver
{
public IServiceProvider ServiceProvider { get; }
public RoleBasedContractResolver( IServiceProvider sp)
{
this.ServiceProvider = sp;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you're using the Identity, you can get the userManager :
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
// ...
}
}
thus we can get the HttpContext
and User
as we like. If you're using the Identity, you can also get the UserManager
service and roles.
and now we can follow @dbc's advice to control the ShouldSerialize
:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you use the Identitiy, you can get the usermanager
//UserManager<IdentityUser>
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
JsonProperty property = base.CreateProperty(member, memberSerialization);
// get the attributes
var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
// if no [RequireResoveView] decorated, always serialize it
if(attrs.Count()==0) {
property.ShouldDeserialize = instance => true;
return property;
}
// custom your logic to dertermine wether should serialize the property
// I just use check if it can statisify any the condition :
var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
property.ShouldSerialize = instance => {
var resource = new { /* any you need */ };
return attrs.Any(attr => {
var rolename = attr.Role;
return roles.Any(r => r == rolename ) ;
}) ? true : false;
};
return property;
}
The function GetIdentityUserRolesAsync
here is helper method to retrieve roles using the current HttpContext
and the UserManger
service :
private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
var rolesCached= context.Items["__userRoles__"];
if( rolesCached != null){
return (IList<string>) rolesCached;
}
var identityUser = await userManager.GetUserAsync(context.User);
var roles = await userManager.GetRolesAsync(identityUser);
context.Items["__userRoles__"] = roles;
return roles;
}
How to inject the IServiceProvider
in details :
The trick is all about how to configure the default MvcJwtOptions
with an IServiceProvider
.
Don't configure the JsonOptions
by :
services.AddMvc().
.AddJsonOptions(o =>{
// o.
});
as it doesn't allow us add a IServiceProvider
parameter.
We can custom a subclass of MvcJsonOptions
:
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}
and register the services by :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// don't forget to add the IHttpContextAccessor
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
Test Case :
Let's say you have a custom POCO :
public class TestObject
{
public string Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public string Field2 => "ViewRequiresAdmin";
[RequireRoleView("HR"),RequireRoleView("OP")]
public string Field3 => "ViewRequiresHROrOP";
[RequireRoleView("IT"), RequireRoleView("HR")]
public string Field4 => "ViewRequiresITOrHR";
[RequireRoleView("IT"), RequireRoleView("OP")]
public string Field5 => "ViewRequiresITOrOP";
}
And the Current User has roles : Admin
and HR
:
The result will be :
{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}
A screenshot of testing with an action method :
edited Nov 14 '18 at 8:37
answered Nov 14 '18 at 8:23
itminusitminus
3,4961321
3,4961321
1
Yaaaay, it works! I was having some issues with using UserManager.GetRolesAsync (service not registered, etc.), so I wound up falling back to getting user roles by parsing the claims, but otherwise it's pretty slick. I also set up some extension methods, so using it is just a matter of adding theRequireRoleView
attribute and callingservices.AddRoleBasedContractResolver()
inStartup.ConfigureServices
. Thanks to both of you :) I've accepted yours as the answer and I'll post my own with my tweaks in case anyone in the future finds it useful.
– user1874135
Nov 14 '18 at 15:53
add a comment |
1
Yaaaay, it works! I was having some issues with using UserManager.GetRolesAsync (service not registered, etc.), so I wound up falling back to getting user roles by parsing the claims, but otherwise it's pretty slick. I also set up some extension methods, so using it is just a matter of adding theRequireRoleView
attribute and callingservices.AddRoleBasedContractResolver()
inStartup.ConfigureServices
. Thanks to both of you :) I've accepted yours as the answer and I'll post my own with my tweaks in case anyone in the future finds it useful.
– user1874135
Nov 14 '18 at 15:53
1
1
Yaaaay, it works! I was having some issues with using UserManager.GetRolesAsync (service not registered, etc.), so I wound up falling back to getting user roles by parsing the claims, but otherwise it's pretty slick. I also set up some extension methods, so using it is just a matter of adding the
RequireRoleView
attribute and calling services.AddRoleBasedContractResolver()
in Startup.ConfigureServices
. Thanks to both of you :) I've accepted yours as the answer and I'll post my own with my tweaks in case anyone in the future finds it useful.– user1874135
Nov 14 '18 at 15:53
Yaaaay, it works! I was having some issues with using UserManager.GetRolesAsync (service not registered, etc.), so I wound up falling back to getting user roles by parsing the claims, but otherwise it's pretty slick. I also set up some extension methods, so using it is just a matter of adding the
RequireRoleView
attribute and calling services.AddRoleBasedContractResolver()
in Startup.ConfigureServices
. Thanks to both of you :) I've accepted yours as the answer and I'll post my own with my tweaks in case anyone in the future finds it useful.– user1874135
Nov 14 '18 at 15:53
add a comment |
Itminus's answer covers everything that's needed, but for anyone interested, I've extended it a little for easy reuse.
First, in a class library
My RequireRoleViewAttribute, which allows multiple roles (OR, not AND):
[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
public List<String> AllowedRoles { get; set; }
public RequireRoleViewAttribute(params String AllowedRoles) =>
this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}
My resolver is almost identical to Itminus's, but CreateProperty is adjusted to
IEnumerable<String> userRoles = this.GetIdentityUserRoles();
property.ShouldSerialize = instance =>
{
// Check if every attribute instance has at least one role listed in the user's roles.
return attrs.All(attr =>
userRoles.Any(ur =>
attr.AllowedRoles.Any(ar =>
String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
);
};
And GetIdentityUserRoles doesn't use UserManager
private IEnumerable<String> GetIdentityUserRoles()
{
IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext context = contextAccessor.HttpContext;
ClaimsPrincipal user = context.User;
Object rolesCached = context.Items["__userRoles__"];
if (rolesCached != null)
{
return (List<String>)rolesCached;
}
var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
context.Items["__userRoles__"] = roles;
return roles;
}
And I have an extensions class which contains:
public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
return services;
}
Then in my API
I reference that class library. In Startup.cs -> ConfigureServices, I call:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddRoleBasedContractResolver();
...
}
And my DTOs are tagged with the attribute:
public class Diagnostics
{
public String VersionNumber { get; set; }
[RequireRoleView("admin")]
public Boolean ViewIfAdmin => true;
[RequireRoleView("hr")]
public Boolean ViewIfHr => true;
[RequireRoleView("hr", "admin")]
public Boolean ViewIfHrOrAdmin => true;
}
And the return value as an admin is:
{
"VersionNumber": "Debug",
"ViewIfAdmin": true,
"ViewIfHrOrAdmin": true
}
add a comment |
Itminus's answer covers everything that's needed, but for anyone interested, I've extended it a little for easy reuse.
First, in a class library
My RequireRoleViewAttribute, which allows multiple roles (OR, not AND):
[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
public List<String> AllowedRoles { get; set; }
public RequireRoleViewAttribute(params String AllowedRoles) =>
this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}
My resolver is almost identical to Itminus's, but CreateProperty is adjusted to
IEnumerable<String> userRoles = this.GetIdentityUserRoles();
property.ShouldSerialize = instance =>
{
// Check if every attribute instance has at least one role listed in the user's roles.
return attrs.All(attr =>
userRoles.Any(ur =>
attr.AllowedRoles.Any(ar =>
String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
);
};
And GetIdentityUserRoles doesn't use UserManager
private IEnumerable<String> GetIdentityUserRoles()
{
IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext context = contextAccessor.HttpContext;
ClaimsPrincipal user = context.User;
Object rolesCached = context.Items["__userRoles__"];
if (rolesCached != null)
{
return (List<String>)rolesCached;
}
var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
context.Items["__userRoles__"] = roles;
return roles;
}
And I have an extensions class which contains:
public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
return services;
}
Then in my API
I reference that class library. In Startup.cs -> ConfigureServices, I call:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddRoleBasedContractResolver();
...
}
And my DTOs are tagged with the attribute:
public class Diagnostics
{
public String VersionNumber { get; set; }
[RequireRoleView("admin")]
public Boolean ViewIfAdmin => true;
[RequireRoleView("hr")]
public Boolean ViewIfHr => true;
[RequireRoleView("hr", "admin")]
public Boolean ViewIfHrOrAdmin => true;
}
And the return value as an admin is:
{
"VersionNumber": "Debug",
"ViewIfAdmin": true,
"ViewIfHrOrAdmin": true
}
add a comment |
Itminus's answer covers everything that's needed, but for anyone interested, I've extended it a little for easy reuse.
First, in a class library
My RequireRoleViewAttribute, which allows multiple roles (OR, not AND):
[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
public List<String> AllowedRoles { get; set; }
public RequireRoleViewAttribute(params String AllowedRoles) =>
this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}
My resolver is almost identical to Itminus's, but CreateProperty is adjusted to
IEnumerable<String> userRoles = this.GetIdentityUserRoles();
property.ShouldSerialize = instance =>
{
// Check if every attribute instance has at least one role listed in the user's roles.
return attrs.All(attr =>
userRoles.Any(ur =>
attr.AllowedRoles.Any(ar =>
String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
);
};
And GetIdentityUserRoles doesn't use UserManager
private IEnumerable<String> GetIdentityUserRoles()
{
IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext context = contextAccessor.HttpContext;
ClaimsPrincipal user = context.User;
Object rolesCached = context.Items["__userRoles__"];
if (rolesCached != null)
{
return (List<String>)rolesCached;
}
var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
context.Items["__userRoles__"] = roles;
return roles;
}
And I have an extensions class which contains:
public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
return services;
}
Then in my API
I reference that class library. In Startup.cs -> ConfigureServices, I call:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddRoleBasedContractResolver();
...
}
And my DTOs are tagged with the attribute:
public class Diagnostics
{
public String VersionNumber { get; set; }
[RequireRoleView("admin")]
public Boolean ViewIfAdmin => true;
[RequireRoleView("hr")]
public Boolean ViewIfHr => true;
[RequireRoleView("hr", "admin")]
public Boolean ViewIfHrOrAdmin => true;
}
And the return value as an admin is:
{
"VersionNumber": "Debug",
"ViewIfAdmin": true,
"ViewIfHrOrAdmin": true
}
Itminus's answer covers everything that's needed, but for anyone interested, I've extended it a little for easy reuse.
First, in a class library
My RequireRoleViewAttribute, which allows multiple roles (OR, not AND):
[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
public List<String> AllowedRoles { get; set; }
public RequireRoleViewAttribute(params String AllowedRoles) =>
this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}
My resolver is almost identical to Itminus's, but CreateProperty is adjusted to
IEnumerable<String> userRoles = this.GetIdentityUserRoles();
property.ShouldSerialize = instance =>
{
// Check if every attribute instance has at least one role listed in the user's roles.
return attrs.All(attr =>
userRoles.Any(ur =>
attr.AllowedRoles.Any(ar =>
String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
);
};
And GetIdentityUserRoles doesn't use UserManager
private IEnumerable<String> GetIdentityUserRoles()
{
IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext context = contextAccessor.HttpContext;
ClaimsPrincipal user = context.User;
Object rolesCached = context.Items["__userRoles__"];
if (rolesCached != null)
{
return (List<String>)rolesCached;
}
var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
context.Items["__userRoles__"] = roles;
return roles;
}
And I have an extensions class which contains:
public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
return services;
}
Then in my API
I reference that class library. In Startup.cs -> ConfigureServices, I call:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddRoleBasedContractResolver();
...
}
And my DTOs are tagged with the attribute:
public class Diagnostics
{
public String VersionNumber { get; set; }
[RequireRoleView("admin")]
public Boolean ViewIfAdmin => true;
[RequireRoleView("hr")]
public Boolean ViewIfHr => true;
[RequireRoleView("hr", "admin")]
public Boolean ViewIfHrOrAdmin => true;
}
And the return value as an admin is:
{
"VersionNumber": "Debug",
"ViewIfAdmin": true,
"ViewIfHrOrAdmin": true
}
answered Nov 14 '18 at 16:12
user1874135user1874135
8712
8712
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53288633%2fnet-core-api-custom-json-resolver-based-on-request-values%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
Can you pass the
userRoles
intoTestObject
when it is constructed? If all your objects that haveRequireRoleViewAttribute
applied could haveuserRoles
as an internal property, you could make a custom contract resolver that does the necessary checks in a customJsonProperty.ShouldSerialize
predicate.– dbc
Nov 13 '18 at 20:28
1
Yeah, my hesitance on ShouldSerialize is in potential for name changes and stuff like that in the future - changing a property meaning we need to remember to change ShouldSerialize, etc. I can probably update
MyCustomResolver
to check for that property on the object being serialized though, instead of needing the roles passed to it when called. I'll poke around with that and see how it goes. Thanks!– user1874135
Nov 13 '18 at 20:39
2
Just to be clear, I meant that the custom contract resolver itself could add a synthetic
ShouldSerialize
predicate based on the current user roles and value forRequireRoleView
. I.e. the logic would be similar to the contract resolver from this answer but rather than doing the checks directly inCreateProperty
,CreateProperty()
would add aShouldSerialize
predicate that would check against object's roles.– dbc
Nov 13 '18 at 20:46