I would like to build a ‘User’ Object model for a somewhat typical web application…however I cannot decide how best to design the object model & role system.
Basically I plan to have about 4 user types…which will correspond to user ‘roles’ in the membership provider.
These types will be:
• Worker
• Employer
• Guest
• Admin
The super type is:
• User
In addition – a User could be both a ‘Worker’ & an ‘Employer’ at times.
I would like to use the MS Roles & Membership provider & have navigation UI set to respond to User Role.
My question is:
How can I best design these Users to be flexible (User can be Worker & Employer).
How do I handle the Login / Roles Procedure?
(I am thinking about a User with a Factory for ‘Behavior’ objects (worker behavior, Employer Behavior ) )
For Login-User logins in … finds its role and Casts to its subtype.
Is this how it should be done?
Using just the concept of role by itself has always proven to be in adequate for me. It doesn't provide low enough granularity to control permissions. AS an example you may have a worker role and and an admin role and then in code you use principal.IsInRole("Admin") to check their role to determine if they can modify some value (say salary). Then someone changes their mind and says that supervisors can change salaries but still aren't admins. Now you have to go change you access check to add in another role check. Painful and routine.
So what I do is make a list of all the features in the application and then allow them to be associated in to a role all in the database. The my access checks look like principal.HasPermission("CHANGESALARY"). I load up the users permissions based on the role they are attached to when they log in. This way the business can create as many groups of features they want and name them. They can then be applied to any user.
I create a custom principal object and attach it to the thread so that I can use it in any code throughout the page life cycle. This object has the code for loading the permissions from the database and the methods for checking permissions.
I generally find that the "providers" in the framework are good for a small class of applications and come up short for most needs. By the time you are done bending them to your will, it would have been easier to just write it from scratch.
To be honest, this is probably not a very good solution, but it might help to generate some other ideas.
My Roles are all of the possible combinations of permissions:
Worker, Employee, Guest, Admin, WorkerEmployee, etc
In my code I have an enum for the individual permissions
[Flags]
public enum RolePermissions
{
Guest = 1,
Worker = 2,
Employee = 4,
Admin = 8
}
and I have an enum that corresponds to the Roles in the database. The integer values are the bitwise OR of permissions:
public enum AvailableRoles
{
None = 0,
Guest = RolePermissions.Guest, //1
Worker = RolePermissions.Worker, // 2
Employee = RolePermissions.Employee, // 4
WorkerEmployee = RolePermissions.Worker | RolePermissions.Employee, // 6
Admin = RolePermissions.Admin, // 8
}
Then there's a set of methods I can use to look up permissions and whatnot:
// Used to determine if the currently logged in user has a particular permission (Guest, Worker, Employee, Admin)
public static bool UserHasPermission( RolePermissions rolePermssion )
{
foreach( string role in Roles.GetRolesForUser() )
{
AvailableRoles availableRole = Parse( role );
if( ( (RolePermissions)availableRole & rolePermssion ) == rolePermssion )
return true;
}
return false;
}
// Used to determine whether the currently logged in user is in a specific role
public static bool UserIsInRole( AvailableRoles requestedRole )
{
return UserIsInRole( Membership.GetUser().UserName, requestedRole );
}
// Used to determine whether a specific user is in a specific role
public static bool UserIsInRole( string username, AvailableRoles requestedRole )
{
foreach( string role in Roles.GetRolesForUser( username ) )
{
AvailableRoles actualRole = Parse( role );
if( actualRole == requestedRole )
return true;
}
return false;
}
// Helper method to parse enum
private static AvailableRoles Parse( string role )
{
return (AvailableRoles)Enum.Parse( typeof( AvailableRoles ), role );
}
If you come up with a better method or make improvements, please let me know so I can incorporate it back into my own code. :-)
Related
My ASP.NET Core web app is using an Azure Active Directory tenant and using OpenID Connect to sign-in users. I'm able to login successfully and I'm able to view the full list of Claims on a user with the following code:
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
My security token includes the following "groups":
{
type: "groups",
value: "e8f1a447-336a-47bb-8c26-79f1183f989f"
},
{
type: "groups",
value: "38421450-61ba-457b-bec2-e908d42d6b92"
}
I'm having trouble trying to determine how to capture these groups to perform logic in my Razor views and controllers. For example, I need to hide/show a button in my Razor view depending on whether a user is in a specific group. In my controllers I may need to allow/deny an action.
What is the standard/preferred method to do this in ASP.NET Core?
When Azure AD adds applicable group claims to the token it issues for users, the value for the group claim will be the Object ID of the security group and not the name of the security group(a group’s name can be changed in the directory so it is not a reliable identifier for the group ) .You could check whether the user’s existence in the security group in controller by :
// Look for the groups claim for the 'Dev/Test' group.
const string devTestGroup = "99dbdfac-91f7-4a0f-8eb0-57bf422abf29";
Claim groupDevTestClaim = User.Claims.FirstOrDefault(
c => c.Type == "groups" &&
c.Value.Equals(devTestGroup, StringComparison.CurrentCultureIgnoreCase));
// If the app has write permissions and the user is in the Dev/Test group...
if (null != groupDevTestClaim)
{
//
// Code to add the resource goes here.
//
ViewBag.inGroup = true;
}
else
{
ViewBag.inGroup = false;
}
Then in view , you could control whether show/hide links/buttons :
#if (ViewBag.inGroup)
{
<div>show/hide button/link goes here</div>
}
In your AppSettings.json, add your group's name and GUID object ID:
"AzureAdAuthorizationGroups": {
"MyGroup": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}
Next, hook up authorisation in your Startup.cs ConfigureServices method
services.AddAuthorization(options => {
options.AddPolicy("MyGroup", policyBuilder => policyBuilder.RequireClaim("groups", Configuration.GetValue<string>("AzureAdAuthorizationGroups:MyGroup")));
});
Finally in your view:
#if ((await AuthorizationService.AuthorizeAsync(User, "MyGroup")).Succeeded)
{
// ...
}
I have designed an application which brings the users from the active directory to an MySQL database, and shows them on GUI. It also brings the groups of which a user is a member of.
So, my program works this way:
for(String domain : allConfiguredADomains) {
LdapContext domainCtx = getDomainCtx(domain);
// Bring all users from this domain and store them in DB
getAllUsersForDomain(domain, domainCtx);
// Bring all the groups for every user
getAllGroupsForUsersInTheDomain(domain, domainCtx)
}
void getAllUsersForDomain(String domain, LdapContext domainCtx) {
String filter = "(objectClass=User)"
NamingEnumeration<SearchResult> result = domainCtx.search(domain, filter, ..);
while(result.hasMoreElements()) {
SearchResult searchResult = (SearchResult) result.nextElement();
// Process and store in database
storeUserInDatabase(searchResult);
}
}
void getAllGroupsForUsersInTheDomain(String domain, LdapContext domainCtx) {
List<String> userDistinguishedNames = getAllUsersFromDatabase("distinguishedName");
for(String userDn : userDistinguishedNames) {
String filter = "(&(objectClass=Group)(distinguishedName=" + userDn + "))";
NamingEnumeration<SearchResult> result = domainCtx.search(domain, filter, ..);
List<String> allGroupsOfUser = new List<String>();
while(result.hasMoreElements()) {
SearchResult searchResult = (SearchResult) result.nextElement();
String groupDistinguishedName = searchResult.getAttributes().get("distinguishedName").get();
allGroupsOfUser.add(groupDistinguishedName);
}
// Store them in database
storeAllGroupsOfUserInDatabase(userDn, allGroupsOfUser);
}
}
This application, however, takes lot of time, when there are too many users in the active directory. So, I decided to implement parallelism (using Threading). I divided this using search filter on distinguishedName of a user.
String filter = "(&(objectClass=User)(distinguishedName=a*"))";
and so on.. in each thread while fetching users.
I got better performance, but still not so good. Can someone suggest
a better way ?
Also, I don't have an idea how can I introduce
parallelism while fetching groups ?
If someone has any suggestions to do this better with powershell or C#, please suggest, I am open to technology.
Please note: reading user attribute memberOf does not provide all groups, hence I am fetching groups separately.
I'm not an Active Directory expert - just wanted to share some thoughts.
Threading by alphabet letter allows a maximum of 26 threads. Have you considered creating search threads by some other attributes, group membership etc? This might let you create more threads.
Review the Active Directory docs to see whether there is a way to improve search performance (for example, with a database we could create an index).
I have a Controller action (the Controller has $this->securityContext set to $this->get('security.context') via JMSDiExtraBundle):
$user = $this->securityContext->getToken()->getUser();
$groupRepo = $this->getDoctrine()->getRepository('KekRozsakFrontBundle:Group');
if ($this->securityContext->isGranted('ROLE_ADMIN') === false) {
$myGroups = $groupRepo->findByLeader($user);
} else {
$myGroups = $groupRepo->findAll();
}
When I log in to the dev environment and check the profiler, I can see that I have the ROLE_ADMIN role granted, but I still get the filtered list of Groups.
I have put some debugging code in my Controller, and Symfony's RoleVoter.php. The string representation of the Token in my Controller ($this->securityContext->getToken()) and the one in RoleVoter.php are the same, but when I use $token->getRoles(), I get two different arrays.
My Users and Roles are stored in the database via the User and Role entities. Is this a bug that I found or am I doing something wrong?
Finally got it. A dim idea hit my mind a minute ago. The problem was caused my own RoleHierarchyInterface implementation. My original idea was to copy Symfony's own, but load it from the ORM instead of security.yml. But because of this, I had to totally rewrite the buildRoleMap() function. The diff is as follows:
private function buildRoleMap()
{
$this->map = array();
$roles = $this->roleRepo->findAll();
foreach ($roles as $mainRole) {
$main = $mainRole->getRole();
- $this->map[$main] = array();
+ $this->map[$main] = array($main);
foreach ($mainRole->getInheritedRoles() as $childRole) {
$this->map[$main][] = $childRole->getRole();
// TODO: This is one-level only. Get as deep as possible.
// BEWARE OF RECURSIVE NESTING!
foreach ($childRole->getInheritedRoles() as $grandchildRole) {
$this->map[$main][] = $grandchildRole->getRole();
}
}
}
}
This case - roles are set and are displayed in Symfony's profiler but isGranted returns false - can be happened when the role names does not start with prefix ROLE_.
Bad role name: USER_TYPE_ADMIN
Correct role name: ROLE_USER_TYPE_ADMIN
Assume we have a report called SalesSummary for a large department. This department has many smaller teams for each product. People should be able to see information about their own product, not other teams' products. We also have one domain group for each of these teams.
Copying SalesSummary report for each team and setting the permission is not the best option since we have many products. I was thinking to use a code similar to below on RS, but it doesn't work. Apparently, System.Security.Principal.WindowsPrincipal is disabled by default on RS.
Public Function isPermitted() As Boolean
Dim Principal As New System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent())
If (Principal.IsInRole("group_prod")) Then
Return true
Else
Return false
End If
End Function
I also thought I can send the userID from RS to SQL server, and inside my SP I can use a code similar to below to query active directory. This also doesn't work due to security restriction.
SELECT
*
FROM OPENQUERY(ADSI,'SELECT cn, ADsPath FROM ''LDAP://DC=Fabricam,DC=com'' WHERE objectCategory=''group''')
Is there any easier way to achieve this goal?
Thanks for the help!
The first option you suggested (using embedded code to identify the executing user) will not be reliable. SSRS code is not necessarily executed as the user accessing the report, and may not have access to that users credentials, such as when running a subscription.
Your second approach will work, but requires the appropriate permissions for your SQL server service account to query Active Directory.
Another approach is to maintain a copy of the group membership or user permissions in a SQL table. This table can be updated by hand or with an automated process. Then you can easily incorporate this into both available parameters and core data queries.
So I ended up with this code:
PrincipalContext domain = new PrincipalContext(ContextType.Domain, "AD");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, identityName);
//// if found - grab its groups
if (user != null)
{
PrincipalSearchResult<Principal> _groups = null;
int tries = 0;
//We have this while because GetGroups sometimes fails! Specially if you don't
// mention the domain in PrincipalContext
while (true)
{
try
{
_groups = user.GetGroups();
break;
}
catch (Exception ex)
{
logger.Debug("get groups failed", ex);
if (tries > 5) throw;
tries++;
}
}
// iterate over all groups, just gets groups related to this app
foreach (Principal p in _groups)
{
// make sure to add only group principals
if (p is GroupPrincipal)
{
if (p.Name.StartsWith(GROUP_IDENTIFIER))
{
this.groups.Add((GroupPrincipal)p);
this.groupNames.Add(p.Name);
}
}
}
}
Now, that you have a list of related group you can check the list to authorize the user!
I need to retrieve all SPUser's from a SPGroup. Unfortunately, the group may contain Active Directory groups, so a simple SPGroup.Users is not enough (I'd just get a single SPUser for the AD group, with the IsDomainGroup property set to true).
Does anyone have a good idea how can I obtain a list of all SPUser's, descending into any Active Directory groups contained in a SPGroup? Is there an alternative to SPGroup.ContainsCurrentUser that takes a SPUser parameter?
Based on a blog post I found, I have written the following code:
private static List<SPUser> ListUsers(SPWeb web, SPPrincipal group)
{
try
{
web.Site.CatchAccessDeniedException = false;
var users = new List<SPUser>();
foreach(SPUser user in web.SiteUsers)
{
using(var userContextSite = new SPSite(web.Site.ID, user.UserToken))
{
try
{
using (var userContextWeb = userContextSite.OpenWeb(web.ID))
{
try
{
if (userContextWeb.SiteGroups[group.Name]
.ContainsCurrentUser)
users.Add(user);
}
catch (SPException)
{
// group not found, continue
}
}
}
catch(UnauthorizedAccessException)
{
// user does not have right to open this web, continue
}
}
}
return users;
}
finally
{
web.Site.CatchAccessDeniedException = true;
}
}
I don't like the fact that I have to impersonate every single user, and this code will only find AD users that have already been imported into SharePoint (so an SPUser exists for them), but that's good enough for me.
Unfortunately, it may be the case that not every member of the AD group has a corresponding SPUser object in the site (yet).
In this scenario, I'd enumerate all the members of the active directory group, and force them into the site with the SPWeb's EnsureUser() method, which returns an SPUser, and creates a new one if it doesn't already exist in the site.
For guidance on enumerating active directory members, see Get List of Users From Active Directory In A Given AD Group.