How to overload UserManager.AddToRoleAsync(string userId, string role)

蓝咒 提交于 2019-12-06 07:17:04

问题


I'm using Asp.net Identity Framework 2.1. I implement customized ApplicatoinUser, ApplicationRole, ApplicationUserRole, because I want to add support to multi-tenant, that is each user belongs to different companies, but I have 3 roles among all these companies, they are User, Admin and Approver.

My ApplicationUserRole derived from IdentityUserRole, and have one more property: CompanyId. This property will indicate the user's role in this particular company. My code for these customized classes attached in bottom.

My question is when I try to override ApplicationUserManager(Yes, it derived from UserManager too)'s AddToRoleAsync , IsInRoleAsync , I don't know how to deal with the new CompanyId, looks like the existing function doesn't receive these companyId(or tenantId).

Then when I'm trying to overload these functions with companyId included, I can't find the db context either in ApplicatoinUserManager nor its base class.

Am I on the right track of adding tenantId/companyId to the application Role?

I've referenced this answer: SO linkes, and this blog.ASP.NET Web Api and Identity 2.0 - Customizing Identity Models and Implementing Role-Based Authorization

My IdentityModels:

public class ApplicationUserLogin : IdentityUserLogin<string> { }
public class ApplicationUserClaim : IdentityUserClaim<string> 
{
}
public class ApplicationUserRole : IdentityUserRole<string> 
{
    public string CompanyId { get; set; }
}

// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class ApplicationUser : IdentityUser<string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>//, IAppUser
{
    public ApplicationUser()
    {
        this.Id = Guid.NewGuid().ToString();
    }
    public virtual string CompanyId { get; set; }
    public virtual List<CompanyEntity> Company { get; set; }
    public DateTime CreatedOn { get; set; }

    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(ApplicationUserManager manager, string authenticationType)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
        // Add custom user claims here
        return userIdentity;
    }
}

// Must be expressed in terms of our custom UserRole:
public class ApplicationRole : IdentityRole<string, ApplicationUserRole>
{
    public ApplicationRole() {}
    public ApplicationRole(string name) : this()
    {
        this.Name = name;
    }

    // Add any custom Role properties/code here
    public string Description { get; set; }
}

// Most likely won't need to customize these either, but they were needed because we implemented
// custom versions of all the other types:
public class ApplicationUserStore: UserStore<ApplicationUser, ApplicationRole, string,ApplicationUserLogin, ApplicationUserRole,ApplicationUserClaim>, IUserStore<ApplicationUser, string>, IDisposable
{
    public ApplicationUserStore()
        : this(new IdentityDbContext())
    {
        base.DisposeContext = true;
    }

    public ApplicationUserStore(DbContext context)
        : base(context)
    {
    }
}

public class ApplicationRoleStore
: RoleStore<ApplicationRole, string, ApplicationUserRole>,
IQueryableRoleStore<ApplicationRole, string>,
IRoleStore<ApplicationRole, string>, IDisposable
{
    public ApplicationRoleStore()
        : base(new IdentityDbContext())
    {
        base.DisposeContext = true;
    }

    public ApplicationRoleStore(DbContext context)
        : base(context)
    {
    }
}

My IdentityConfig:

public class ApplicationUserManager
    : UserManager<ApplicationUser, string>
{
    public ApplicationUserManager(IUserStore<ApplicationUser, string> store)
        : base(store) { }

    public static ApplicationUserManager Create(
        IdentityFactoryOptions<ApplicationUserManager> options,
        IOwinContext context)
    {
        var manager = new ApplicationUserManager(
            new UserStore<ApplicationUser, ApplicationRole, string,
                ApplicationUserLogin, ApplicationUserRole,
                ApplicationUserClaim>(context.Get<ApplicationDbContext>()));

        // Configure validation logic for usernames
        manager.UserValidator = new UserValidator<ApplicationUser>(manager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = false
        };

        // Configure validation logic for passwords
        manager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6,
            //RequireNonLetterOrDigit = true,
            //RequireDigit = true,
            //RequireLowercase = true,
            //RequireUppercase = true,
        };
        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            manager.UserTokenProvider =
                new DataProtectorTokenProvider<ApplicationUser>(
                    dataProtectionProvider.Create("ASP.NET Identity"));
        }

        // add sms and email service provider
        manager.SmsService = new EMaySmsServiceProvider();
        manager.EmailService = new ConcordyaEmailServiceProvider();

        return manager;
    }
    public string GetCurrentCompanyId(string userName)
    {
        var user = this.FindByName(userName);
        if (user == null)
            return string.Empty;

        var currentCompany = string.Empty;
        if (user.Claims.Count > 0)
        {
            currentCompany = user.Claims.Where(c => c.ClaimType == ConcordyaPayee.Core.Common.ConcordyaClaimTypes.CurrentCompanyId).FirstOrDefault().ClaimValue;
        }
        else
        {
            currentCompany = user.CurrentCompanyId;
        }
        return currentCompany;
    }

    public override Task<IdentityResult> AddToRoleAsync(string userId, string role, string companyId)
    { 
        return base.AddToRoleAsync(userId, role);
    }

    #region overrides for unit tests
    public override Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
    {
        return base.CheckPasswordAsync(user, password);
    }

    public override Task<ApplicationUser> FindByNameAsync(string userName)
    {
        return base.FindByNameAsync(userName);
    }
    #endregion
}

public class ApplicationRoleManager : RoleManager<ApplicationRole>
{
    public ApplicationRoleManager(IRoleStore<ApplicationRole, string> roleStore)
        : base(roleStore)
    {

    }

    public static ApplicationRoleManager Create(
        IdentityFactoryOptions<ApplicationRoleManager> options,
        IOwinContext context)
    {
        return new ApplicationRoleManager(
            new ApplicationRoleStore(context.Get<ApplicationDbContext>()));
    }
}

回答1:


First of all, I would like to say thanks for taking it this far. It gave me a great start for my multi-tenant roles solution. I'm not sure if I'm 100% right, but this works for me.

Firstly, you cannot override any of the "RoleAsync" methods, but you can overload them. Secondly, the UserStore has a property called "Context" which can be set to your DbContext.

I had to overload the "RoleAsyc" methods in both my UserStore and UserManager extended classes. Here is an example from each to get you going:

MyUserStore

    public class MyUserStore : UserStore<MyUser, MyRole, String, IdentityUserLogin, MyUserRole, IdentityUserClaim> {

        public MyUserStore(MyDbContext dbContext) : base(dbContext) { }

        public Task AddToRoleAsync(MyUser user, MyCompany company, String roleName) {
            MyRole role = null;

            try
            {
                role = Context.Set<MyRole>().Where(mr => mr.Name == roleName).Single();
            }
            catch (Exception ex)
            {
                throw ex;
            }

            Context.Set<MyUserRole>().Add(new MyUserRole {
                Company = company,
                RoleId = role.Id,
                UserId = user.Id
            });

            return Context.SaveChangesAsync();
        }
    }

MyUserManager

    public class MyUserManager : UserManager<MyUser, String>
    {
        private MyUserStore _store = null;

        public MyUserManager(MyUserStore store) : base(store)
        {
            _store = store;
        }

        public Task<IList<String>> GetRolesAsync(String userId, int companyId)
        {
            MyUser user = _store.Context.Set<MyUser>().Find(new object[] { userId });
            MyCompany company = _store.Context.Set<MyCompany>().Find(new object[] { companyId });

            if (null == user)
            {
                throw new Exception("User not found");
            }

            if (null == company)
            {
                throw new Exception("Company not found");
            }

            return _store.GetRolesAsync(user, company);
        }
    }

From here a couple scary things happen and I don't know a better way to manage them.

  1. The User "IsInRole" method in the HttpContext will work but it will not be tenant-sensitive so you can no longer use it.
  2. If you use the "Authorize" attribute, the same idea for "scary thing 1" applies, but here you can just extend it and make things happy for your system. Example below:

MyAuthorizeAttribute

    public class MyAuthorizeAttribute : AuthorizeAttribute {
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (null == httpContext)
            {
                throw new ArgumentNullException("httpContext");
            }

            HttpSessionStateBase session = httpContext.Session;
            IList<String> authorizedRoleNames = Roles.Split(',').Select(r => r.Trim()).ToList();

            if (!httpContext.User.Identity.IsAuthenticated)
            {
                return false;
            }

            if (null == session["MyAuthorize.CachedUsername"])
            {
                session["MyAuthorize.CachedUsername"] = String.Empty;
            }

            if (null == session["MyAuthorize.CachedCompanyId"])
            {
                session["MyAuthorize.CachedCompanyId"] = -1;
            }

            if (null == session["MyAuthorize.CachedUserCompanyRoleNames"])
            {
                session["MyAuthorize.CachedUserCompanyRoleNames"] = new List<String>();
            }

            String cachedUsername = session["MyAuthorize.CachedUsername"].ToString();
            int cachedCompanyId = (int)session["MyAuthorize.CachedCompanyId"];
            IList<String> cachedUserAllRoleNames = (IList<String>)session["MyAuthorize.CachedUserAllRoleNames"];

            IPrincipal currentUser = httpContext.User;
            String currentUserName = currentUser.Identity.Name;
            int currentCompanyId = (int)session["CurrentCompanyId"];//Get this your own way! I used the Session in the HttpContext.

            using (MyDbContext db = MyDbContext.Create())
            {
                try
                {
                    MyUser mUser = null;
                    ICollection<String> tmpRoleIds = new List<String>();

                    if (cachedUsername != currentUserName)
                    {
                        session["MyAuthorize.CachedUsername"] = cachedUsername = String.Empty;

                        //Reload everything
                        mUser = db.Users.Where(u => u.Username == currentUserName).Single();

                        session["MyAuthorize.CachedUsername"] = currentUserName;
                        session["MyAuthorize.CachedCompanyId"] = cachedCompanyId = -1; //Force Company Reload
                        cachedUserCompanyRoleNames.Clear();
                    }

                    if (cachedUserCompanyRoleNames.Count != db.Users.Where(u => u.Username == currentUserName).Single().Roles.Select(r => r.RoleId).ToList().Count)
                    {
                        cachedUserCompanyRoleNames.Clear();

                        if (0 < currentCompanyId)
                        {
                            if(null == mUser)
                            {
                                mUser = db.Users.Where(u => u.Username == cachedUsername).Single();
                            }

                            tmpRoleIds = mUser.Roles.Where(r => r.Company.Id == currentCompanyId).Select(r => r.RoleId).ToList();

                            session["MyAuthorize.CachedUserCompanyRoleNames"] = cachedUserCompanyRoleNames = db.Roles.Where(r => tmpRoleIds.Contains(r.Id)).Select(r => r.Name).ToList();

                            session["MyAuthorize.CachedCompanyId"] = cachedCompanyId = currentCompanyId;
                        }
                    }

                    if (cachedCompanyId != currentCompanyId)
                    {
                        cachedUserCompanyRoleNames.Clear();

                        //Reload company roles
                        if (0 < currentCompanyId)
                        {
                            if(null == mUser)
                            {
                                mUser = db.Users.Where(u => u.Username == cachedUsername).Single();
                            }

                            tmpRoleIds = mUser.Roles.Where(r => r.Company.Id == currentCompanyId).Select(r => r.RoleId).ToList();

                            session["MyAuthorize.CachedUserCompanyRoleNames"] = cachedUserCompanyRoleNames = db.Roles.Where(r => tmpRoleIds.Contains(r.Id)).Select(r => r.Name).ToList();

                            session["MyAuthorize.CachedCompanyId"] = cachedCompanyId = currentCompanyId;
                        }
                    }
                }
                catch (Exception ex)
                {
                    return false;
                }
            }

            if (0 >= authorizedRoleNames.Count)
            {
                return true;
            }
            else
            {
                return cachedUserCompanyRoleNames.Intersect(authorizedRoleNames).Any();
            }
        }
    }

In closing, as I said, I'm not sure if this is the best way to do it, but it works for me. Now, throughout your system, make sure you used your overloaded methods when dealing with Roles. I am also thinking about caching the Roles in a MVC BaseController that I wrote so that I can get similar functionality to User.IsInRole in all of my MVC Views.



来源:https://stackoverflow.com/questions/29071800/how-to-overload-usermanager-addtoroleasyncstring-userid-string-role

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!