I have a simple sandbox project I'm using to get to better understand how .net Core Identity works and I've come across a bit of an inconsistency that I hope someone can explain. This project is using Entity Framework.
I used this awesome article to help me set up the project, https://medium.com/#goodealsnow/asp-net-core-identity-3-0-6018fc151b4#.2env44446 and my User class is as follows.
public class User : IdentityUser<int>
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string TempPassword { get; set; }
}
I seeded the db with three users and three roles, one user for each role, "Owner", "Admin", and "User". I added some policies for my actions,
auth.AddPolicy("Owner", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole("Owner");
});
auth.AddPolicy("Admin", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole("Admin", "Owner");
});
auth.AddPolicy("User", policy =>
{
policy.RequireAuthenticatedUser();
});
so my attributes like [Authorize("Admin")] work great. I even added some principal extensions as so
public static class PrincipalExtensions
{
public static bool IsOwner(this ClaimsPrincipal principal)
{
return principal.IsInRole("Owner");
}
public static bool IsAdmin(this ClaimsPrincipal principal)
{
return principal.IsInRole("Admin") || principal.IsInRole("Owner");
}
public static bool IsUser(this ClaimsPrincipal principal)
{
return principal.Identity.IsAuthenticated;
}
}
so I can do if(User.IsAdmin()) and this works perfectly as well.
Here is where it gets weird...
If I step through the following code I get confusing results.
var user = await _userManager.GetUserAsync(User);
var userRoles = await _userManager.GetRolesAsync(user);
await _userManager.AddToRoleAsync(user, "Owner");
The first line gets me a User object for the principal. On that object there is a collection of his Roles, user.Roles, but it will show empty (Count = 0) even though the user does have roles.
The second line gets the Roles for the user and it populates correctly.
The third line adds the "Owner" role to the user and it works correctly (the db is updated) but also, the local variable user suddenly now has that role in user.Roles! Note, none of the user's other roles will show up, just that one.
So I have basically two questions: 1. Why doesn't the user object have the user.Roles populated to begin with? 2. Why is it suddenly synced after I add a role?
Any help is appreciated.
Your Roles collection isn't populated after calling GetUserAsync() as the EntityFramework Identity UserStore doesn't request the information. It's doing the equivalent of you accessing the user data directly through your DbContext and without any calls to Include().
Right now EF Core does not support lazy loading, and therefore the user.Roles navigation property isn't automatically populated. And yes, this makes the behaviour somewhat disingenuous at the moment.
In your calls to GetRolesAsync() and AddToRoleAsync() the data is being explicitly populated for you, as you are operating on the roles directly.
Related
I am developing an internal MVC Application using Windows Authentication (WA). Authenticating users using WA is straight forward, however; with respect to user Roles, I have the following requirements:
We will use custom Roles ignoring the AD Roles. For example, a user
may have a 'Manager' role in the AD but his app role is set to
'Supervisor'. After the User is authenticated, the system will fetch
the user roles and set the CurrentPrincipal accordingly.
For the above, I plan to have 3 tables including User, Role
and UserRole. The Role table has the custom roles while the
User table consists of company users. The UserRole table will define
the mapping between User and their Role(s). The issue I see with this approach
is to pre-populate all 3 tables. The User table must have the list of all
company employees and is maintained for new/inactive employees. The UserRole
table should be set with each user and his role(s) before he logs in.
In the application, User are assigned to different tasks (for example John is
supervising Vehicles) plus we need to maintain user activity logs. Assuming
the above two points are valid, is it OK to use the ID field in the User
table for this purpose?
There is also a chance that later, we may deploy the application
over the public domain. In such a case, how can we use the existing
User/Role infrastructure for this purpose.
Thanks in advance.
You are in exactly the same boat as me, my friend! I managed to do this through a Custom Authorization Attribute. Here are a couple of points that I have stumbled on through this process.
I did not create my own user table. You can, but you can query AD for users depending on the amount of users on your domain and link it to your Roles / Activities tables using the Guid to search. If you do create a users table that mirrors AD, use the Guid for the user. This way, if the login/name/anything else changes, the Guid stays the same.
Custom authorization attribute:
namespace YourSite.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public class AuthRoleAttribute : ActionFilterAttribute
{
public string RoleName { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!ViewIsAuthorizedActivity(RoleName)) // create a bool function to see if your user is in this role... check your db or whatever
{
string requestMethod = filterContext.HttpContext.Request.HttpMethod;
if (requestMethod == "GET")// I chose two different means of forbidding a user... this just hides the part of the page based on the #if(ViewBag.Authorization = "FORBIDDEN") where you render a partial, else show the view
{
filterContext.Controller.ViewBag.Authorization = "FORBIDDEN";
}
else if (requestMethod == "POST") // This prevents over posting by redirecting them completely away from that controller... also prevents them from posting if they have the page loaded and you remove permission
{ // Hiding a part of the page doesn't matter for the POST if the page is already loaded
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Home" },
{ "action", "Forbidden" },
{ "area", ""}
});
}
base.OnActionExecuting(filterContext);
}
}
}
}
How GETs are handled in the view:
#if (ViewBag.Authorization == "FORBIDDEN")
{
ViewBag.Title = "Forbidden!";
#Html.Partial("~/Views/Forbidden.cshtml");
}
else
<!-- User is not forbidden and have the view here -->
Note that for the POSTs the user is redirected away from the controller to the Forbidden controller.
Attribute on controller:
[AuthRole(RoleName = "Admin")]
public ActionResult YourController()
I also made a extension to the User so things may be hidden in the view if they don't have permission:
public static bool IsAuthorized(this IPrincipal user, string roleName)
{
return Attributes.AuthActivityAttribute.ViewIsAuthorizedByRole(roleName); // function determining if the user is in that role, therefore show what you want to show in the view or don't show it if false
}
Which is called by:
#if (User.IsAuthorized("Admin"))
{
<!-- show something, a link, etc. -->
}
Hopefully this gives you a better head start than I had. Let me know if you have questions.
we keep fighting with out multi tenant application.
This is an ASP MVC EF6 Code First web application.
We initialize a list of tenants in the Application_Start, getting a pair of values:
Host
TenantId
So we can associate any host with one TenantId, and store that list in cache.
We have configured a custom filter to get the current tenant.
public class TenantActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.HttpContext.Items.Add("TenantId", GetCurrentTenant(filterContext.HttpContext.Request.Url.Host));
base.OnActionExecuting(filterContext);
}
}
The GetCurrentTenant function just access the list in cache and get the current one based on the host passed.
Is it correct to store the current tenant in an item in the context?
After that, we have created an Interceptor to get any query and add a filter to filter by TenantId. This is done and working good, we just need to add the tenantId from the context:
The problem we have is where we get the TenantId for each request.
if (HttpContext.Current.CurrentHandler == null) return;
var clientId = Convert.ToInt32(HttpContext.Current.Items["ClientId"]);
foreach (DbParameter param in command.Parameters)
{
if (param.ParameterName != TenantAwareAttribute.TenantIdFilterParameterName)
continue;
param.Value = clientId;
}
We don't know if this is the correct approach since there is a lot of informationon the net.
Thanks.
In my experience, the persistence of the tenant Id in the HTTP context is not right, as in some cases, the HTTP context becomes null.
You can try to get the tenant Id from the claims of the current principal. Creating a static class with a tenant identifier property that reads from the claims and gives is more reliable. Assuming you are using the owin pipeline, this should be easy to do. You can take a look at the reference sample application from github here
It looks like the below block,
public static class UserContext
{
public static string TenantId
{
get
{
return Threading.Thread.CurrentPrincipal.FindFirst("tenantid");
}
}
}
I have a code first model with a mapping table so that I can map MenuItem to an IdentityRole, enabling the production of a menu based on the logged in users role assignment.
public class MenuItem
{
public int Id { get; set; }
public string Text { get; set; }
}
public class MenuRoleMap
{
[Key]
public int Id { get; set; }
public virtual MenuItem MenuItem { get; set; }
public virtual IdentityRole Role { get; set; }
}
The IdentityRole and rest of Identity is auto wired via IdentityDbContext which I've inherited through my ApplicationDbContext like this, then the context should be consistent.
public class ApplicationDbContext : IdentityDbContext<User>
All of the tables look right, they have the expected columns and foreign keys, here is the MenuRoleMap table
I have an valid existing instance of MenuItem and IdentityRole which I use to try and add a new entity item to this table
foreach (IdentityRole role in selectedRoles)
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
db.MenuRoleMaps.Add(mrm);
}
db.SaveChanges(); /// <<<=== HERE ERROR BECAUSE THE role IS ALREADY IN DB
Which throws this error
A first chance exception of type 'System.Data.Entity.Validation.DbEntityValidationException' occurred in EntityFramework.dll
Role: Role SystemsAdministrator already exists.
Which of course, it does exist, I know that, it's already in the database. Surely the EF should not be trying to add a new entity item for the foreign key entity if it already exists?
It doesn't do it for the MenuItem, only the IdentityRole.
I thought the problem was proxy creation since the IdentityRole was a proxy object, so I turned that off
this.Configuration.ProxyCreationEnabled = false;
but I still get the same error.
My question is, how do you add an entity where the foreign key is an IdentityRole?
Thank you stackers.
ANSWER TO MY OWN QUESTION
After assistance from those below I discovered while investigating the various solutions that the problem wasn't one of context per se but the validity of an object. The object looked right, what I hadn't realised is that it wasn't the object from the context, it was a facsimile. By trying to add this facsimile to the model, the context quite rightly says it already exists, you can't add it again. By trying to override the state of the item I created a different kind of error.
The resolution was simply to reload the object from the context and then add that to the parent item like so
foreach (IdentityRole role in selectedRoles)
{
// Here I'm getting the role from the context using the ID I have from the facsimile
IdentityRole roleToUse = db.Roles.Where(x => x.Id == role.Id).FirstOrDefault();
// carry on as normal
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = roleToUse; // note I'm using the retrieved 'roleToUse'
db.MenuRoleMaps.Add(mrm);
}
db.SaveChanges();
Hey presto it all works.
It looks like you've retrieved the entity from another context and then assigned it to an entity which is then added to a different context. It would then try to insert the Role entity too.
Are you returning the Role from another method where the lifetime of the context is scoped to that method?
You may find the following link useful in regards to updating the state of objects:
Entity states and SaveChanges
The problem is that when you use db.Set<MyEntity>.Add you will mark all entities that are attached to the entity that is added as being added too. You have to explicitly mark them as being unchanged:
foreach (IdentityRole role in selectedRoles)
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
db.MenuRoleMaps.Add(mrm);
db.Entry(role).State=EntityState.Unchanged;
}
db.SaveChanges();
My answer is you can't or at least should not.
Authentication (Roles), and Business (Menu) are different concerns of the application.
For me you have to bring in the ApplicationDb, the part of IdentityDb that you need and organize the synchronisation.
To illustrate my saying: Imaging you use Google or LiveID as authentification provider: can you imagine navigation properties from you ApplicationDd to Google or Microsoft Dbs ?
Clearly not.
So create a AppRole replicating the Role of the authentication database and use this table from your application database to build your menus.
In pseudo code this looks like:
List<Int32> l = IdentityContext.GetRolesForUser(currentUserId);
foreach (AppRole role in AppContext.Roles.Where(r => l.Contains(r.Id)))
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
appContext.MenuRoleMaps.Add(mrm);
}
appContext.SaveChanges();
Another solution would be to use the same context for Application and Identity.
Inheritance of context seems fine, but I never tested it.
I have the following tables/views
Users (View)
-> UserId
-> Name
Roles (Table)
-> RoleId
-> Name
UserRoles (Table)
-> UserId
-> RoleId
and the classes
public class Role{
public int RoleId{get;set}
public string Name{get;set}
}
public class User{
public int UserId{get;set}
public string Name{get;set}
public ICollection<Role> Roles{get;set}
}
and the save method
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.SaveChanges();
}
As you can see above, the Users is a view and UserRoles is a mapping table. I am able to retrieve User entities along with the mapped Roles. But while saving it is not throwing any exceptions nor is it saving. I tried checking the db using profiler and it is not even hitting the db.
Since Users is a view I don't want to save the User entity but only the changes made in the Roles collection.
This cannot save anything. Attach puts the whole object graph into the context, but in state Unchanged. If all objects in the context are unchanged SaveChanges won't issue any command to the DB (because for EF nothing has changed).
If you want to make changes which EF recognizes as such you must first attach the object which represents the state in the Db and then make your changes, something like:
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.Roles.Attach(myNewRole);
user.Name = myNewName;
user.Roles.Add(myNewRole);
// etc.
helper.SaveChanges();
}
Alternatively you can mark the user as modified:
helper.Entry(user).State = EntityState.Modified;
But I believe this only affects scalar properties of the entity and it doesn't solve the problem to add a new role to the user.
I'm using VS2010, EF4 feature CTP (latest release), and POCO objects, such as the example below:
class Person
{
public int ID { get; set; }
public string Name { get; set; }
public virtual IList<Account> Accounts { get; set; }
...
}
class Account
{
public string Number { get; set; }
public int ID { get; set; }
...
}
For the sake of brevity, assume context below is the context object for EF4. I have a dbml mapping between entity types and the database, and I use it like this with no problem:
Person doug = context.Persons.CreateObject();
doug.Name = "Doug";
context.Add(doug);
context.Save();
doug.Accounts.Add(new Account() { Name = "foo" });
context.Save(); // two calls needed, yuck
At this point, the database has a Person record with the name "Doug", and an account record "foo". I can query and get those record back just fine. But if I instead try to add the account before I save the Person, the Accounts list is null (the proxy hasn't created an instance on that property yet). See the next example:
Person doug = context.Persons.CreateObject();
doug.Name = "Doug";
doug.Accounts.Add(new Account() { Name = "foo" }); // throws null reference exception
context.Add(doug);
context.Save();
Has anybody else encountered this? Even better, has anyone found a good solution?
Person doug = context.Persons.CreateObject();
doug.Name = "Doug";
context.Add(doug);
doug.Accounts.Add(new Account() { Name = "foo" });
context.Save();
This will work
Yes and yes!
When you new the POCO up (as opposed to CreateObject from the Context), no proxies are provided for you. This may seem obvious, but I had to explicitly remind myself of this behavior when chasing a similar issue down. (I know this isn't the situation you described in the question, but the overall issue should be acknowledged).
Initializing collections in the constructor of the POCO does not interfere with proper EF4 proxy lazy-loading behavior, from what I've observed in my own testing.
OK, all this being said, I now see your comment to the previous answer -- why don't I have a proxied Addresses collection when I request a new Person from my context? Do you have lazy loading enabled on the context? Seeing how we're dealing with navigation properties, I could see where having lazy loading turned off may make a difference in this situation.
ISTM that if you expect the framework to do all this for you then you wouldn't really have a "POCO", would you? Take your Person class, with the code above. What would you expect the state of the Accounts property to be after construction, with no constructor, if the EF weren't involved? Seems to me that the CLR will guarantee them to be null.
Yes, proxies can initialize this when necessary for materialization of DB values, but in the EF, "POCO" actually means "Plain". Not "something packed with runtime-generated code which we pretend is 'Plain'".