LDAP_MATCHING_RULE_IN_CHAIN not working with default AD groups - Domain Users - vb.net

In my program, I need to fetch all the AD groups for a user.
The current version of my program uses System.DirectoryServices.AccountManagement.UserPrincipal.GetAuthorizationGroups.
This works good until we had a new customer with a much largeer AD. There it is really slow. (Up to 60 seconds)
Now I've been looking around, and saw the posts that the AccountManagement is easy to use, but slow.
I also found that LDAP_MATCHING_RULE_IN_CHAIN should also fetch all the nested groups that a user is member of. And is more performant.
From this link.
But I'm having an issue with the default groups that in AD exists.
For example the group "Domain Users" is not returned by the function.
They also have a group "BDOC" that as member have "Domain Users". That group is also not returned.
Trough the GetAuthorizationGroups it is returned correct.
I'm using following code to fetch the groups by user.
VB.NET:
Dim strFilter As String = String.Format("(member:1.2.840.113556.1.4.1941:={0})", oUserPrincipal.DistinguishedName)
Dim objSearcher As New DirectoryServices.DirectorySearcher("LDAP://" & oLDAPAuthenticationDetail.Domain & If(Not String.IsNullOrWhiteSpace(oLDAPAuthenticationDetail.Container), oLDAPAuthenticationDetail.Container, String.Empty))
objSearcher.PageSize = 1000
objSearcher.Filter = strFilter
objSearcher.SearchScope = DirectoryServices.SearchScope.Subtree
objSearcher.PropertiesToLoad.Add(sPropGuid)
objSearcher.PropertiesToLoad.Add(sPropDisplayName)
Dim colResults As DirectoryServices.SearchResultCollection = objSearcher.FindAll()
Afterwards I was testing with the script from the link, if it was possible to fetch all the users from the Domain Users group, by changing the "member" to "memberOf" in the filter.
When I put the Domain Admins group in the filter, it shows the admins correct.
When I put the Domain Users group in the filter, it returns nothing.
Powershell:
$userdn = 'CN=Domain Users,CN=Users,DC=acbenelux,DC=local'
$strFilter = "(memberOf:1.2.840.113556.1.4.1941:=$userdn)"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://rootDSE")
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = "LDAP://$($objDomain.rootDomainNamingContext)"
$objSearcher.PageSize = 1000
$objSearcher.Filter = $strFilter
$objSearcher.SearchScope = "Base"
$colProplist = "name"
foreach ($i in $colPropList)
{
$objSearcher.PropertiesToLoad.Add($i) > $nul
}
$colResults = $objSearcher.FindAll()
foreach ($objResult in $colResults)
{
$objItem = $objResult.Properties
$objItem.name
}
I don't know what I'm doing wrong. Or is it maybe just not possible to fetch the "default groups" with that filter?
What is a good alternative then?

The default group is odd. It is not stored in memberOf, or even in the member attribute of the group. That's why your search won't find it. The default group is determined by the primaryGroupId of the user. That attribute stores the RID (the last part of the SID) of the group. It's kind of dumb, I know :)
I actually wrote an article about the 3 (yes 3) different ways someone can be a member of a group: What makes a member a member?
I also wrote an article about getting all of the groups a single user belongs to, and how to account for all 3 ways: Finding all of a user’s groups
For example, here is the C# code I put in that article about how to find the name of the primary group for a user (given a DirectoryEntry). It shouldn't be too hard to translate that to VB.NET:
private static string GetUserPrimaryGroup(DirectoryEntry de) {
de.RefreshCache(new[] {"primaryGroupID", "objectSid"});
//Get the user's SID as a string
var sid = new SecurityIdentifier((byte[])de.Properties["objectSid"].Value, 0).ToString();
//Replace the RID portion of the user's SID with the primaryGroupId
//so we're left with the group's SID
sid = sid.Remove(sid.LastIndexOf("-", StringComparison.Ordinal) + 1);
sid = sid + de.Properties["primaryGroupId"].Value;
//Find the group by its SID
var group = new DirectoryEntry($"LDAP://<SID={sid}>");
group.RefreshCache(new [] {"cn"});
return group.Properties["cn"].Value as string;
}
You are right that the AccountManagement namespace makes things easy, but it really does have terrible performance sometimes. I never use it anymore. I find that DirectoryEntry/DirectorySearcher gives you much more control of how often your code makes calls out to AD.
I have been meaning to write an article about writing high performance code with DirectoryEntry, but I haven't gotten around to it yet.
Update: So if you need the nested groups for the user, including membership through the primary group, then you can find the primary group first, then do an LDAP_MATCHING_RULE_IN_CHAIN search for groups that have both the user and the primary group as members:
(|(member:1.2.840.113556.1.4.1941:={userDN})(member:1.2.840.113556.1.4.1941:={primaryGroupDN}))
Update: If you want to include Authenticated Users in your search (edit the DC portion of the distinguishedName):
(|(member:1.2.840.113556.1.4.1941:=CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=domain,DC=com)(member:1.2.840.113556.1.4.1941:={userDN})(member:1.2.840.113556.1.4.1941:={primaryGroupDN})
Note that you can also use the tokenGroups attribute of a user to find out all of the authentication groups for a user. The tokenGroups attribute is a constructed attribute, so you only get it if you specifically ask for it (with DirectoryEntry.RefreshCache() or, in a search, add it to DirectorySearcher.PropertiesToLoad.
The caveat is that tokenGroups is a list of SIDs, not distinguishedName, but you can bind directly to an object using the SID with the LDAP://<SID={sid}> syntax.

Related

Finding duplicate users in a user list (Asked in an Interview)

I was recently asked this in an interview for a SDE role.
Suppose you have a list of User objects
class User {
String userId;
String email;
String ip_addr;
}
where userId field is unique among all users, while ip_addr and email are not necessarily so.
and you know some users have more than one user account (if any two User objects share a common email OR an ip_addr, you classify them as belonging to the same user).
you are required to write a function, whose signature is given as:
List<List<User>> findDups(User[] userList) {
// code here
}
[EDIT] so for example, if there are 7 users, only 2 of which are unique, the function can return something like the following (not necessarily in this specific order):
{
{user1, ip1, email1},
{user5, ip5, email1},
{user24, ip5, email2}
},
{
{user2, ip2, email2},
{user7, ip2, email7},
{user8, ip2, email0},
{user19, ip19, email7}
}
here, in this first group, the first user (user1) is the same user as the second one (user5) as they share the same email address. We also know that the third user (user24) is also the same user as it shares the same ip address (ip5) as the second user in the list.
[/END EDIT]
what data structure would you use and what would be the time complexity of the proposed solution?
I tried to use disjoint set union (quick union), which would give me linear complexity, however the interviewer constantly tried to steer me away from that and said just the Collection API would be enough (Using Lists and Sets and maps).
What would be your approach and solution and the corresponding time complexity?

How do I implement, for instance, "group membership" many-to-many in Parse.com REST Cloud Code?

A user can create groups
A group had to have created by a user
A user can belong to multiple groups
A group can have multiple users
I have something like the following:
Parse.Cloud.afterSave('Group', function(request) {
var creator = request.user;
var group = request.object;
var wasGroupCreated = group.existed;
if(wasGroupCreated) {
var hasCreatedRelation = creator.relation('hasCreated');
hasCreatedRelation.add(group);
var isAMemberOfRelation = creator.relation('isMemberOf');
isAMemberOfRelation.add(group);
creator.save();
}
});
Now when I GET user/me with include=isMemberOf,hasCreated, it returns me the user object but with the following:
hasCreated: {
__type: "Relation"
className: "Group"
},
isMemberOf: {
__type: "Relation"
className: "Group"
}
I'd like to have the group objects included in say, 'hasCreated' and 'isMemberOf' arrays. How do I pull that using the REST API?
More in general though, am I approaching this the right way? Thoughts? Help is much appreciated!
First off, existed is a function that returns true or false (in your case the wasGroupCreated variable is always going to be a reference to the function and will tis always evaluate to true). It probably isn't going to return what you expect anyway if you were using it correctly.
I think what you want is the isNew() function, though I would test if this works in the Parse.Cloud.afterSave() method as I haven't tried it there.
As for the second part of your question, you seem to want to use your Relations like Arrays. If you used an array instead (and the size was small enough), then you could just include the Group objects in the query (add include parameter set to isMemberOf for example in your REST query).
If you do want to stick to Relations, realise that you'll need to read up more in the documentation. In particular you'll need to query the Group object using a where expression that has a $relatedTo pointer for the user. To query in this manner, you will probably need a members property on the Group that is a relation to Users.
Something like this in your REST query might work (replace the objectId with the right User of course):
where={"$relatedTo":{"object":{"__type":"Pointer","className":"_User","objectId":"8TOXdXf3tz"},"key":"members"}}

yii rbac: check autorizations on groups instead of users

I have a question about the rbac system. I think I've pretty well understood it but I need more informations about a special case.
I would like to do the autorisations on groups instead of users. I mean for instance the group "HR" has permission to create a person. Then any person who join this group would have it as well.
Let me give you more informations.
A part of my database:
And this a part of what my group hierarchy could be:
So what I'm looking for, this would be a must, is a system where each group has some autorizations. People get the autorizations of their group and of their parents group (for instance people in "Forsys" has the autorizations of "Forsys", "R&D" and "Administration").
The solution I see at the moment is using bizrule. But I'm not sure write php code in database is a good idea and then if I update the group hierarchy (R&D inherits of RH instead of Administration) I would have to modify bizrule in database. I tried it and it works well but as you can see it require a lot of code.
$user = User::model()->with("people","people.groups")->findByPk(Yii::app()->user->id);
foreach($user->people[0]->groups as $group)
if($group->id == 2)
return true;
return false;
It's just for see if a user is in a group (without checking parent groups and hierarchy)
Another possibility could be create a new table "group_auth" where we would say for instance:
-Group_2 has role "managePerson"
-Group_3 has operation "deleteUser"
...
And then everytime a user is added in or removed of a group we would update his autorizations in the auth_assigment table.
I'd like to hear other opinions on this subject.
All comments will be appreciated :)
Thank you for reading and sorry for my English if you had difficulties to understand me.
Michaël S.
Do users ever get their own authorization items? If not, seems like you could in essence swap out the userid column in auth_assignment and name it / treat it as groupID instead. That way you wouldn't need to worry about keeping user auth assignments in sync with your group roles.
A couple of places you'd probably need to make some changes:
- by default CWebUser passes in the logged in userid for use in bizrules. Might be good to change that our with your own override that passes in groupId/groupIds instead.
- you'd need to override CDbAuthManager and rework some of how things work there
We've done something similar on a project I've worked on (we were handling multi-tenant RBAC custom permissions), which required custom CDbAuthManager overrides. It gets a bit tricky if you do it, but there is an awful lot of power available to you.
Edit:
Understood about your users sometimes needing to have additional authorizations. What if your group has a 'roles' field with different roles serialized in it (or some other method of having multiple roles stored for that group, could also be a relationship).
Then, on user login (for efficiency), you'd store those roles in session. Probably the easiest way to handle things would be to write a custom checkAccess for your WebUser override:
https://github.com/yiisoft/yii/blob/1.1.13/framework/web/auth/CWebUser.php#L801
as that will make things simpler to do your custom checking. Then I'd probably do something like:
if(Yii::app()->user->hasGroupAccess() || Yii::app()->user->checkAccess('operation/task/role')) {
....
}
In your WebUser hasGroupAccess method, you could loop over all group roles and send those to checkAccess as well.
Think that will work?
What I use to check access for groups when it's in another table, or somewhere else in the application I give the user the role per default. By using this:
return array(
'components'=>array(
'authManager'=>array(
'class'=>'CDbAuthManager',
'defaultRoles'=>array('authenticated', 'R&D', 'Administration'),
),
),
);
Under: Using Default Roles
By using this, every user gets these assignments. Now, I create a business rule to make sure that the checkAccess('group') will return the correct value.
For example in your case the business rule for R&D would be:
return (
count(
Person::model()->findByPk(Yii::app()->user->id)->groups(array('name'=>'R&D'))
) > 0
) ? true : false;
So what this does is:
find the logged-in person by primary key
look into groups (from the user) for the group with name R&D
if there is a group: return true (else return false)

Make openam/opensso return role name instead of role universal id

I'm using OpenAM 9.5.2 for authenticating users on an application. The authentication works well but I'm having issues to get user memberships from final application.
I've defined the group "somegroup" in openam and added my user to this group. Now in my application, I want to test if authenticated users is member of this group. If I'm testing it with:
request.isUserInRole("somegroup");
I get false result. Actually, I have to test
request.isUserInRole("id=somegroup,ou=group,dc=opensso,dc=java,dc=net");
in order to get a true response.
I know that it's possible to define a privileged attribute mapping list in the sso agent configuration to map id=somegroup,ou=group,dc=opensso,dc=java,dc=net on somegroup, but it's not suitable in my situation since roles and groups are stored in an external database. It's not convenient to define role in database and mapping in sso agent conf.
So my question : is there a way to make openam use the "short" (i.e. somegroup) group name instead of its long universal id ?
This is not an answer, just one remark.
I've performed some researches in openam sources and it seems to confirm that the role name stored in repository is replaced by universalId when openam build the request. This is performed in com.sun.identity.idm.server.IdRepoJAXRPCObjectImpl.java class:
public Set getMemberships_idrepo(String token, String type, String name,
String membershipType, String amOrgName,
String amsdkDN
) throws RemoteException, IdRepoException, SSOException {
SSOToken ssoToken = getSSOToken(token);
Set results = new HashSet();
IdType idtype = IdUtils.getType(type);
IdType mtype = IdUtils.getType(membershipType);
Set idSet = idServices.getMemberships(ssoToken, idtype, name, mtype, amOrgName, amsdkDN);
if (idSet != null) {
Iterator it = idSet.iterator();
while (it.hasNext()) {
AMIdentity id = (AMIdentity) it.next();
results.add(IdUtils.getUniversalId(id));
}
}
return results;
}
To my knowledge this is not possible currently with out of box code. In case you have limited amount of groups, then privileged attribute mapping could be a way to go, but if not, then the issue gets more complicated.
You could try to change the AmRealm implementation (authenticateInternal method) to match your requirements and hook the new class into the container-specific ServiceResolver class (like http://sources.forgerock.org/browse/openam/trunk/opensso/products/j2eeagents/tomcat/v6/source/com/sun/identity/agents/tomcat/v6/AmTomcatAgentServiceResolver.java?r=700&r=700&r=700 )
You can also create a JIRA issue about providing a config property to put membership information into roles in non-UUID format.

"Domain Users" group is empty when I use DirectoryServices "member" property

I'm using the following code to get the members of a group on my domain:
Dim de As New DirectoryEntry("LDAP://" & GroupDN)
For Each user As String In CType(de.Properties("member"), IEnumerable)
GroupCollection.Add(Username, Username)
Next
My problem is that when GroupDN (the distinguishedname of the group) is "CN=Domain Users,CN=Users,DC=Mydomain,DC=local", the For...Each loop doesn't execute, and when I check the Properties statement manually, it's got a count of zero. This seems to work for every other group in my domain, but the "Domain Users" group should contain everybody, and it appears to contain nobody.
I've checked, and the group lists everybody correctly in my Windows AD tools. Is there something obvious that I'm missing here? On a side note, is there a better way to get all the members of a group?
Unless you change the primary group id of a user, the user is not stored in the member attribute of the Domain Users group, rather it uses the fact that the primary group id is set to the Domain Users RID to determine membership in Domain Users. The normal case is that the Domain Users member attribute is empty; it would require that you make some changes to the default Active Directory implementation for this to not be the case.
The Domain Users group uses a
"computed" mechanism based on the
"primary group ID" of the user to
determine membership and does not
typically store members as
multi-valued linked attributes. If the
primary group of the user is changed,
their membership in the Domain Users
group is written to the linked
attribute for the group and is no
longer calculated. This was true for
Windows 2000 and has not changed for
Windows Server 2003.
Reference
The accepted answer is absolutely correct. By default every (user) object has 513 set in the property primarygroupid, which is the fixed "tail" of the Domain Users sid. But: this can be changed and every other group can be configured there, so we can't rely on that.
Here is an example method how to get the group members anyway (regardless if the default is kept or changed). I call a method like this after any query for members of active directory groups. In this example I get an array of distinguished names as result. But all other properties are possible, just add them to dSearcher.PropertiesToLoad.Add(...) and modify the result.
I know, this is a question about VB, I hope its easy to port it.
using System.DirectoryServices;
using System.Security.Principal;
public static string[] GetMembersDnByPrimaryGroupId(string domainName, SecurityIdentifier sidOfGroupToGetMembersByPrimaryGroupId)
{
// In a single domain environement the domain name is probably not needed, but
// we expect a multy domain environement
if (string.IsNullOrWhiteSpace(domainName) || sidOfGroupToGetMembersByPrimaryGroupId == null)
{
throw new ArgumentNullException($"Neither domainName nor sid may be null / blank: DomainName: { domainName }; sid: { sidOfGroupToGetMembersByPrimaryGroupId }");
//<----------
}
List<string> membersDnResult = new List<string>();
// Get the last segment of the group sid, this is what is stored in the "primaryGroupId"
string groupSidTail = sidOfGroupToGetMembersByPrimaryGroupId.Value.Split('-').Last();
string path = $"LDAP://{ domainName }";
DirectoryEntry dEntry = new DirectoryEntry(path);
SearchResultCollection adSearchResult = null;
DirectorySearcher dSearcher = new DirectorySearcher(dEntry);
// For this example we need just the distinguished name but you can add
// here the property / properties you want
dSearcher.PropertiesToLoad.Add("distinguishedName");
// set the filter to primarygroupid
dSearcher.Filter = $"(&(primarygroupid={ groupSidTail }))";
// May die thousand deaths, therefore exception handling is needed.
// My exception handling is outside of this method, you may want
// to add it here
adSearchResult = dSearcher.FindAll();
// Get the domains sid and check if the domain part of the wanted sid
// fits the domain sid (necesarry in multy domain environments)
byte[] domainSidBytes = (byte[])dEntry.Properties["objectSid"].Value;
SecurityIdentifier domainSid = new SecurityIdentifier(domainSidBytes, 0);
if (sidOfGroupToGetMembersByPrimaryGroupId.AccountDomainSid != domainSid)
{
throw new ArgumentException($"Domain sid of the wanted group { sidOfGroupToGetMembersByPrimaryGroupId.AccountDomainSid } does not fit the sid { domainSid } of the searched through domain \"{ domainName }\"");
//<----------
}
// We found entries by the primarygroupid
if (adSearchResult.Count > 0)
{
foreach (SearchResult forMemberByPrimaryGroupId in adSearchResult)
{
// Every AD object has a distinguishedName, therefore we acess "[0]"
// wihtout any further checking
string dn = forMemberByPrimaryGroupId.Properties["distinguishedName"][0].ToString();
membersDnResult.Add(dn);
}
}
return membersDnResult.ToArray();
}