Extending Identity 2.1 to assign user to company and roles to users - asp.net-mvc-5

Ok so i am really lost now,
I have an application that works great, i have groups of data that are available to a company subscription, When the user logs in they can access the information from the data that their roles allow, If they have multiple roles they have access to multiple areas of reports, if they only subscribe to one role they only have access to that one area. now all the data is the same and updated daily, so if 100 users are subscribed to roles 1,3 and 5 they can access that data. for the last decade the users of the companies would use one login to access the data ( one login supplied to company for George, But Lisa, john, Jerry and bob all use George's credentials to download reports). Note the data does not change per company and the site does not change per company all data and themes are part of the one site they have access to.
Now the Issue.
I have been asked by many of my users (Companies that access my data) that they would like to be able to to add users to their account so they can monitor who is downloading what report as i charge subscription by 500, 1000, and 2000 reports. My application records reports downloaded and the user can view these reports from their user portal and the id that downloaded it. the issue is that the companies want to be able to administer their own users, sort of like an account admin. I have looked into multi tenant and this does not really suit my needs, but from what i am looking at i feel i need to add a company_id to the user and company_id user roles so each company can have an admin role and a user role. but not sure as i study and read more on this i see allot of different ideas. but dont really see one that works for me.
here is an example.
Each user (company) has many users, each user in that company has roles of Admin or user. Users (companies) have access to many areas that roles allow them and the users of that company would have the same access to reports tha company roles provide. (or see aggregate data across all groups they belong to).
Thinking of adding this to IdentityModels:
public class ApplicationUserRole : IdentityUserRole<string>
{
public string CompanyId { get; set; }
}
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)
{
var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
return userIdentity;
}
}
add to IdentityConfig:
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);
}
Now im not sure about this, do i need to create a new user store. and how would authentication work, will i need to write a new Authorize Attribute. should i be using the claims. or maybe ACL system. or do i need to find a different identity provider.
Any help would be appreciated greatly. as i am getting very confused.

Related

Structuring Data in Firebase Cloud Firestore

I had been trying to structure my database in firestore
my json looks like this
{"UID":"myusrsid","PhotoList":[{"PhotoID":333,"PhotoURL":"url1ofphone"}, {"PhotoID":332,"PhotoURL":"url2ofphoto"} ]}
Here UID is UserID and its unique to users and this UID inside this have Photos List which have photo id and its url
so whenever user upload a new photo if the userid already exist the photo should be added to Photos List
and if new user then UID with new user should be created and photo should be added in photo list
I had been scratching my head to structure this
public class Users
{
public string UID { get; set; }
public List<PhotoList> PhotoList { get; set; }
}
public class PhotoList
{
public int PhotoID { get; set; }
public string PhotoURL { get; set; }
}
this is my model which I am planing to send and receive data
I am aware of retrieving and storing the data the only thing is right now I need help with how should I structure it. I am using Node js
here is what I had been doing right now
const imageBody = {
uid,
imageBase64String,
} = req.body;
await db.collection('users').doc(imageBody.uid).collection('Photos').doc(imageBody.imageBase64String).create({
url : imageBody.imageBase64String
});
Firestore has a maximum document size, so storing binary files is not recommended. Rather use Firebase Storage and store the URL in Firestore.
If you still want to store the images in Firestore consider using a flatter structure. You can still maintain strong access control and it will also be easier for other users share photos. By putting access control in the document itself you can get really powerful sharing capability without sacrificing security. So something like:
Users
Photos
ownerUID
imageBase64String
readPrivs
And to access images for a user:
await db.collection("Photos").where("ownerUID", "==", uid).get();

Razor pages assign roles to users

I am wondering how to create and assign roles in Razor Pages 2.1. application.
I have found how to make them for MVC application (How to create roles in asp.net core and assign them to users and http://hishambinateya.com/role-based-authorization-in-razor-pages), however it does not work for razor pages as I have no IServicesProvider instance.
What I want is just to create admin role and assign it to seeded administrator account. Something similar has been done in this tutorial https://learn.microsoft.com/en-us/aspnet/core/security/authorization/secure-data?view=aspnetcore-2.1, but it seems be sutied for MVC and does not work properly after I applied it to my application. Please help me to understand how to create and seed roles in Razor Pages.
Will be very greatfull for help!
I handle the task next way. First, I used code proposed by Paul Madson in How to create roles in asp.net core and assign them to users. Abovementioned method I have inserted into Startup.cs. It creates administrator role and assigned it to seeded user.
private void CreateRoles(IServiceProvider serviceProvider)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
Task<IdentityResult> roleResult;
string email = "someone#somewhere.com";
//Check that there is an Administrator role and create if not
Task<bool> hasAdminRole = roleManager.RoleExistsAsync("Administrator");
hasAdminRole.Wait();
if (!hasAdminRole.Result)
{
roleResult = roleManager.CreateAsync(new IdentityRole("Administrator"));
roleResult.Wait();
}
//Check if the admin user exists and create it if not
//Add to the Administrator role
Task<ApplicationUser> testUser = userManager.FindByEmailAsync(email);
testUser.Wait();
if (testUser.Result == null)
{
ApplicationUser administrator = new ApplicationUser
{
Email = email,
UserName = email,
Name = email
};
Task<IdentityResult> newUser = userManager.CreateAsync(administrator, "_AStrongP#ssword!123");
newUser.Wait();
if (newUser.Result.Succeeded)
{
Task<IdentityResult> newUserRole = userManager.AddToRoleAsync(administrator, "Administrator");
newUserRole.Wait();
}
}
}
Then, in the same file in Configure method I add argument (IServiceProvider serviceProvider), so you should have something like Configure(..., IServiceProvider serviceProvider). In the end of Configure method I add
CreateRoles(serviceProvider).
To make this code work create ApplicationUser class somwhere, for example in Data folder:
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sobopedia.Data
{
public class ApplicationUser: IdentityUser
{
public string Name { get; set; }
}
}
Finally, inside ConfigureServices method substitute
services.AddIdentity<ApplicationUser>()
.AddEntityFrameworkStores<SobopediaContext>()
.AddDefaultTokenProviders();
with
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<SobopediaContext>()
.AddDefaultTokenProviders();
As a result, after programm starts in table AspNetRoles you will get a new role, while in table AspNetUsers you will have a new user acuiering administrator role.
Unfortunatelly, after you add the following code
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<SobopediaContext>()
.AddDefaultTokenProviders();
pages Login and Registration stop working. In order to handle this problem you may follow next steps:
Scaffold Identity following (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-2.1&tabs=visual-studio).
Then substitute IdentityUser for ApplicationUser in entire solution. Preserv only IdentityUser inheritance in ApplicationUser class.
Remove from Areas/identity/Pages/Account/Register.cs all things related to EmailSernder if you have no its implementation.
In order to check correctness of the roles system you may do as follows. In the end of ConfigureServices method in Startup.cs add this code:
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator"));
});
services.AddMvc().AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Contact","RequireAdministratorRole");
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
If it does not worki then just add [Authorize(Roles = "Administrator")] to Contact Page model, so it will look something like this:
namespace Sobopedia.Pages
{
[Authorize(Roles = "Administrator")]
public class ContactModel : PageModel
{
public string Message { get; set; }
public void OnGet()
{
Message = "Your contact page.";
}
}
}
Now, in order to open Contact page you should be logged in with login someone#somewhere.com and password _AStrongP#ssword!123.

Relation to users when these are stored in an external Identity provider service

I'm trying to create an API and a website client for it. Lately I've been reading a lot about OAuth2 as a security mechanism and companies that offers authentication as a service such as auth0.com or even Azure active Directory and I can see the advantages in using them
Because I'm used to always having the users in the same database and tables with relationships to the Users table in the form of One to Many such as below
public class User
{
public string subjectId { get; set; }
public virtual List<Invoice> Invoices { get; set; }
/*
More properties in here
*/
}
public class Invoice
{
public int InvoiceId { get; set; }
public string PaymentNumber { get; set; }
public DateTime Date { get; set; }
public double Amount { get; set; }
public string Description { get; set; }
public virtual User User { get; set; }
}
My questions is then.
If the users are stored in an external authentication service such as Auth0.com,
How the Invoice class will handle the relation to the user?
Would it be just adding a new property subjectId in the Invoice table and this will take the value of whatever id the authentication service assigned?
In the latter case, would the class Invoice be something like below?
public class Invoice
{
public int InvoiceId { get; set; }
public string PaymentNumber { get; set; }
public DateTime Date { get; set; }
public double Amount { get; set; }
public string Description { get; set; }
public string SubjectId{get;set;}
}
Also, if the users are stored someplace else, how do you make a query like,
Select * from Users u inner join Invoices i where Users.Name='John Doe' and i.Date>Somedate.
Since you have mentioned Auth0 as your Identity provider there are multiple ways to achieve the user table in your database.
1. Authenticating/ registering the user with Auth0 will send a response with Profile Object which will have all the basic profile information you need. Post this profile object back to your own API to save it to database. This API endpoint should be secured with the access token you received along with the profile object from Auth0.
2. You can create a custom rule in Auth0 that posts the user information back to your api. This rule gets executed on Auth0 server so this is a secure call.
3. Identity providers (Auth0 in our case) are required to expose an API endpoint that gives us user profile data (ex: https://yourdoamin.auth0.com/userinfo). You can make a call to this endpoint from your API to receive the user information.
When user Registers to your application, please use one of these techniques to establish a User profile information table in your database. It is always a good idea to treat the Identity Provider as a service responsible for authenticating the resource owner (the user of your application) and providing an access token for securely accessing your API/ application. If you have the profile of the user in your database, you do not have to depend on the Identity Provider once the user is authenticated.
Please let me know if you have any further questions.
Thank you,
Soma.
We have a similar setup for our website. We use Passport for our user database and our website doesn't have a user table at all. This makes life much simpler than having a a bunch of duplicate data between Passport and our website. I'll use our code as an example of what you are doing and hopefully it makes sense.
Our website has a License object that looks like this (Java not C#, but they are similar):
public class License {
public String companyName;
public List<User> users;
}
The License table looks like this (trimmed down):
CREATE TABLE licenses (
id UUID NOT NULL,
company_name VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);
The License identifies the users that are associated with it via a join table like this (Passport uses UUIDs for user ids making life simple again):
CREATE TABLE users_licenses (
users_id UUID NOT NULL,
licenses_id UUID NOT NULL,
PRIMARY KEY (users_id, licenses_id),
CONSTRAINT users_licenses_fk_1 FOREIGN KEY (licenses_id) REFERENCES licenses (id)
);
Then we can select in either direction. If we know the user id, we can ask for all their licenses like this:
select * from licenses where users_id = ?
Or if we know the license id, we can ask for all the users that have access to the license:
select * from users_licenses where licenses_id = ?
Once we have one or more user ids, we can call the Passport /api/user endpoint or the /api/user/search endpoint to retrieve one or more user objects. We are actually using the Passport Java Client (https://github.com/inversoft/passport-java-client) which makes the API call for us and then returns a List<User>. This is what is stored in the License class from above. That code looks like this:
License license = licenseMapper.retrieveById(licenseId);
List<UUID> userIds = licenseMapper.retrieveUserIdsFor(licenseId);
ClientResponse<SearchResponse, Errors> clientResponse = passportClient.searchUsers(userIds);
license.users = clientResponse.successResponse.users;
LicenseMapper is a MyBatis interface that executes the SQL and returns the License objects. C# ORMs use LINQ, but it would be similar.
The nice thing about this setup is that we don't have a user database table in our website database that we have to keep in sync. Everything is loaded from Passport via the API. We aren't ever concerned about performance either. Passport is on-premise and can do thousands of user lookups each second, so we always load the data instead of caching it.
The only piece of your question that requires additional code is handling the joins when you are searching for arbitrary users like name='John Doe'. The only way to handle this is to query your user database first, retrieve all the IDs, then load their invoices. This seems like it could be dangerous if you have a large user database, but still doable.
That could would look like this in our situation:
UserSearchCriteria criteria = new UserSearchCriteria().withName("John Doe");
ClientResponse<SearchResponse, Errors> clientResponse = passportClient.searchUsersByQueryString(criteria);
List<User> users = clientResponse.successResponse.users;
Set<License> licenses = new HashSet<>();
for (User user : users) {
licenses.addAll(licenseMapper.retrieveByUserId(user.id));
}

Can ASP Identity handle multitenancy, I have collisions in role names in ASP Identity web app

In a public facing web app multi tenant app, I'm using ASP Identity 2.x.
I let 3rd party's e.g. non-profits/comps self-register, create and populate their own roles and users. The code below is fine, if its an intranet scenarios where everyone belongs to the same company, it does not for work multiple non-profits
The non-profits registrants (understandably so) are naming roles with the same names, i.e. Managers and Employees etc. which are common/same across the database.
How can I extend ASP Identity to separate the roles per organization in a multi-tenant fashion, can you help me understand the design and how extend this, do I need a sub-role? i.e.
what do I do to ensure roles are scoped per organization at the database, so that different org's can have the same role names?
and, what do I do at the middle tier, i.e. usermanager, role manager objects level (middle tier)
//Roles/Create
[HttpPost]
public ActionResult Create(FormCollection form)
{
try
{
context.Roles.Add(new Microsoft.AspNet.Identity.EntityFramework.IdentityRole()
{ \\ Question - can I add another level here like company??
Name = form["RoleName"]
});
context.SaveChanges();
ViewBag.ResultMessage = "Role created successfully";
return RedirectToAction("RoleCreated");
}
catch
{
return View();
}
}`
Question- When adding a role, how do I separate the role to know which Role is from Which company when I add to the user?
public ActionResult RoleAddToUser(string UserName, string RoleName)
{
ApplicationUser user = context.Users.Where(u => u.UserName.Equals(UserName, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault();
var account = new AccountController();
account.UserManager.AddToRole(user.Id, RoleName);
ViewBag.ResultMessage = "Role created successfully !";
// prepopulat roles for the view dropdown
var list = context.Roles.OrderBy(r => r.Name).ToList().Select(rr => new SelectListItem { Value = rr.Name.ToString(), Text = rr.Name }).ToList();
ViewBag.Roles = list;
return View("ManageUserRoles");
}
how do I get the list of Roles and Users for that non-profit?
public ActionResult ManageUserRoles()
{
var list = context.Roles.OrderBy(r => r.Name).ToList().Select(rr => new SelectListItem { Value = rr.Name.ToString(), Text = rr.Name }).ToList();
ViewBag.Roles = list;
return View();
}`
I'm guessing you do have some sort of TenantId or CompanyId that is an identifier to the tenant you are working with.
IdentityRole and IdentityUser are framework objects that is recommended to inherit to add your own properties. You should do just that:
public class MyApplicationRole : IdentityRole
{
public int CompanyId { get; set; }
}
public class MyApplicationuser : IdentityUser
{
public int CompanyId { get; set; }
}
You can also add reference to a company object here and link it as a foreign key. This might make your life easier retrieving company objects related to the user.
Based on the objects above I'll try answering your questions:
When roles are created you can append a CompanyId to the name as a prefix. Something like <CompanyId>#CustomerServiceRole. This will avoid name clashes. At the same time add CompanyId to the MyApplicationRole class. Prefixed identifier will avoid clashes, identifier in the object will make the roles discoverable by the company. Alternatively you can implement RoleValidator and do the validation based on unique role name within a company. But then you will have to change the code that creates user identity when users are logged in, as role ids are not stored into the cookie.
I think this is self explanatory from the previous answer:
context.Roles.Add(new Microsoft.AspNet.Identity.EntityFramework.IdentityRole()
{
CompanyId = companyId, // TODO Get the company Id
Name = companyId.ToString() + "#" + form["RoleName"]
});
Self explanatory, see code snippets above.
Depending how you link your roles to companies the query will be different. Basically you'll need to do a join between companies and matching roles based on CompanyId and then filter companies by the non-profit flag.

Attaching duplicate existing entities to a new entity

I have a projects table which has three associations to the contacts table, one for each type of contact (Sponsor, Broker, Logistics). I allow a user to select from an existing list of contacts. If the same contact is selected for two or more of these my code gets them correctly pointed to the existing entry, but I get a new unused contact entry in the database.
I only allow the first of any duplicates to be modified, I assemble them detached and then attach to the DbContext prior to saving.
if (newProject.SponsorContact != null && newProject.SponsorContact.Id > 0)
if (AttachContact(newProject.SponsorContact))
SdContext.Entry(newProject.SponsorContact).State = EntityState.Modified;
else
newProject.SponsorContact = _contactDataMapper.GetContactById(newProject.SponsorContact.Id);
if (newProject.BrokerContact != null && newProject.BrokerContact.Id > 0)
if (AttachContact(newProject.BrokerContact))
SdContext.Entry(newProject.BrokerContact).State = EntityState.Modified;
else
newProject.BrokerContact = _contactDataMapper.GetContactById(newProject.BrokerContact.Id);
if (newProject.LogisticsContact != null && newProject.LogisticsContact.Id > 0)
if (AttachContact(newProject.LogisticsContact))
SdContext.Entry(newProject.LogisticsContact).State = EntityState.Modified;
else
newProject.LogisticsContact = _contactDataMapper.GetContactById(newProject.LogisticsContact.Id);
AttachContact just keeps track of the attached contact id's and returns false if already attached so that I can grab the existing contact from the context.
protected bool AttachContact(Contact contact)
{
if (!_attachedContacts.Contains(contact.Id))
{
_attachedContacts.Add(contact.Id);
return true;
}
return false;
}
So if I choose Jacob Marley with ContactId 17 twice, my project saves with both entries pointing to the correctly modified Jacob Marley id 17, but I get a new Jacob Marley also saved in the contact table with no project pointing to that entry. Giving serious consideration to accessing the context during assembly so I know existing entries are already attached.
I am new to C# MVC and entity framework. This may be common knowledge, but if you are reading this your likely having the same issue I have described above.
The key elements are that the model in question has multiple fields that reference the same foreign table and during assembly these were detached models. In my case I have a project table which has three different types of contact and a user may select existing contacts for this which could mean that the same foreign contact is used for all three fields.
So the Project model has an int foreign key field for each one AND a virtual model.
public Nullable<int> SponsorContactId { get; set; }
public Nullable<int> BrokerContactId { get; set; }
public Nullable<int> LogisticsContactId { get; set; }
public virtual Contact SponsorContact { get; set; }
public virtual Contact BrokerContact { get; set; }
public virtual Contact LogisticsContact { get; set; }
Because I built my assembler to cascade down from Organization to Project to Contacts I was unable to assemble the contacts as attached.
There are two ways this can be solved.
The first is to make sure that you assemble them as attached, meaning you get the contact model from the context instead of creating a new contact model.
The second is the route I took. I only allowed a user to edit one of the same contact on the front end and in the view model I tracked if it was modified. During assembly if the contact was existing, meaning it had an Id > 0, I would only assemble the virtual contact if it was also modified. If it was not modified I just populated the int value. So if Sponsor Contact is existing but not modified I set int SponsorContactId and did not assemble the Contact SponsorContact.
If you have any questions please post a comment. I check in regularly and will get back to you.

Resources