How does UserPrincipal.FindByIdentity(PrincipalContext context, string identityValue) query Active Directory? - c#

I am using .NET Framework 4.8 by necessity. I am running into an issue where:
UserPrincipal.FindByIdentity(context, username); is resulting in a System.DirectoryServices.AccountManagement.MultipleMatchesException exception for a specific username. I can retrieve other users just fine.
There are two accounts in my Active Directory that this user utilizes, however, both of them have different UPNs, sAMAccountNames, etc.
I took a look at the IdentityType enum and the FindByIdentity overload that allows for an exact IdentityType look-up. With this information, I used each possible IdentityType and tried the searches:
None of these lines of code result in a System.DirectoryServices.AccountManagement.MultipleMatchesException exception. The Sid and Guid look-ups fail because the username field isn't in the right format, and the other look-ups either pull the user or pull null.
So what am I missing here? Does the overload that leaves out a specific IdentityType do a different look-up than anything that's possible by supplying an IdentityType?
Edit #1: As Alex pointed out, I left out that I tried IdentityType.UserPrincipalName from my screenshot. This was just a cropping mistake. That call also does not throw an exception.

When you don't specify which identifier you're using, it's going to try them all at once. The source code is available now. The code that actually builds the query and executes it is here. Specifically it's line 617 where it starts building the filter. The source for IdentityClaimToFilter is here. Then it puts each condition in an OR.
So if we ignore Guid, Sid, and DistinguishedName, since we know those won't match, this is the relevant LDAP query (assuming the username you're using is "UserName"):
(|(sAMAccountName=UserName)(userPrincipalName=UserName)(name=UserName))
Try making that query in PowerShell and see if you get multiple matches (I'm assuming the computer you run this from is joined to the domain you want to search):
$search = [adsisearcher]"(|(sAMAccountName=UserName)(userPrincipalName=UserName)(name=UserName))"
$search.FindAll()
If the sAMAccountName of one account matches the name of another account, for example, you will get multiple matches.

Related

Active Directory Attribute changing value

I am trying fetch the whenChanged attribute for a user record from Active Directory using the DirectorySearcher class.
It seems the value is changed during or maybe after it is fetched because it is not the same as the value I can see in LDAP.
E.g. For my own profile the value in LDAP reads: 11/29/2022 5:10:21 Eastern Standard Time but after fetching this value through my code, it says 11/29/2022 10:10:24. Please note it is not 10:10 as per my system clock.
The obvious conclusion is that it is a time zone issue. But my system is in EST too. Also the difference in time is different for different users.
Does anyone know why this is happening?
There are a couple things going on.
As mentioned by Selvin, the value is always stored in GMT, which explains the 5-hour difference.
The value is not replicated between domain controllers, which explains the 3-second difference.
To explain the second point in more detail, let's take an example where a user's title is updated. When that update happens, the whenChanged attribute is updated on the DC where the change was made. Then when the new title value is replicated to another DC, that other DC updates the whenChanged attribute to the time when the replication happened. Because of that, the whenChanged value will be different on each DC.
The whenChanged values will usually be pretty close, but they can differ significantly. For example, when you logon, the lastLogon attribute is updated on the DC you authenticated to, along with whenChanged. However, lastLogon is not replicated, so the whenChanged value on all the other DCs will not get updated. That is, unless it's time to update the lastLogonTimestamp value, in which case that will replicate.
So if you're checking the value, make sure you're reading from the same DC. With DirectoryEntry, you can specify the DC you want to use in the LDAP path:
var user = new DirectoryEntry("LDAP://dc1.example.com/CN=someuser,OU=Users,DC=example,DC=com"
, null, null, AuthenticationTypes.ServerBind);
The use of AuthenticationTypes.ServerBind is because the documentation says:
Specifying a server name without also specifying this flag results in unnecessary network traffic.
Or you can get the DC that DirectoryEntry automatically found by using:
var dc = user.Options.GetCurrentServerName();

Active Directory security - is user on computer?

I have a task to select specific group of computers. Their unique property is that their security list contains permission for specific user. In AD tool - right click on computer, Security tab, Group or user names as seen on screenshot:
I can get the ComputerPrincipal object of relevant host, UserPrincipal of user, and both underlying DirectoryEntry objects, but I struggle to make a "join" and find if user is on the list.
I use C#, .NET3.5.
You will have to loop through every group, and for each group you could do something like this (assuming you have a groupDe, which is a DirectoryEntry for the group):
foreach (var identityReference in groupDe.ObjectSecurity.GetAccessRules(true, false, typeof(System.Security.Principal.SecurityIdentifier)) {
var nt_name = identityReference.Translate(typeof(NTAccount)).Value;
//nt_name.Value is now domain\username of the user this permission is for
//so you can match this against your list of users
}
The false in the call to GetAccessRules makes it not look at inherited permissions (I assumed that's what you want).
I haven't tested this, so it might need some tweaking.
This will probably be a very slow process, depending on how many groups you're looking through. You might be able to make it go faster by omitting the call to Translate. identityReference.Value would have (I believe) a SID, so you could try to match by SID rather than translating to something else before matching.

LDAP property contains to many values

I have a function witch tries to remove a member from a group
The problem is if you try to remove a member, without knowing the existence in the group, you could cause an exception.
So I try to enumerate its membership beforehand.
The problem now is that the member property stops after 3000 Entries, and I don't know a way to get more, or the next 3000 members of that group.
Here is my code
DirectoryEntry target_group = new DirectoryEntry(LDAP_group_DN);
if (target_group.Properties["member"].Contains(LDAP_member_to_remove_DN)) {
target_group.Properties["member"].Remove(LDAP_member_to_remove_DN);
}
target_group.CommitChanges();
target_group.Properties["member"] contains exactly 3000 entries, but in reality it is around 7500.
As a shorthand fix I am using the remove statement in a try/catch block without the .Contains() check, but that doesn't seem correct/beautiful/right.
Can anyone lead me to the correct way?
PS: I can not change the structure of our Directory.
This is a Group of RADIUS users, with should not be split up in more groups!
Instead of getting all the group members to determine if the user is part of that list I would use the memberOf/isMemberOf attribute (assuming that your directory supports this feature). This attribute will tell you if a user belongs to a group without having to retrieve all group members.
This other answer might help.
You need to look at into MaxValRange and learn how to retrieve more values using C#.
We have a very simple sample, but, alas, it is in Java

Retrieving friendly name as claims user in SharePoint

I'll explain what I'm trying to do first. I have a claims identifier being passed in, for example, 0e.t|saml provider|first.last#domain.local. I want to trim that to first.last#domain.local.
I am well aware that this can be done with simple string formatting, however, this is not very flexible, so if something gets passed in that I don't expect, the string formatting could fail. It's simply more prone to issues.
I want to do this dynamically instead. Here's what I've tried.
Attempting to EnsureUser with the claims identifier above, then calling SPUser.Name:
SPUser spUser = SPContext.Current.Web.EnsureUser(spUserName);
userName = spUser.Name;
I've tried the following strings as a parameter in EnsureUser, all result in an exception: The user with the specified logonName cannot be found in Active Directory.
0e.t|saml provider|first.last#domain.local
saml provider|first.last#domain.local
first.last#domain.local
Again, all of those fail.
Another approach I tried, using SPClaimProviderManager (pulled from this blog):
SPClaimProviderManager mgr = SPClaimProviderManager.Local;
if (mgr != null && SPClaimProviderManager.IsEncodedClaim(userName))
{
spUserName = mgr.DecodeClaim(SPContext.Current.Web.CurrentUser.LoginName).Value;
}
I want to ensure that I'm attempting to decode an actual claim, and not something else, so I call IsEncodedClaim. This, however, always returns false.
My questions:
1) What am I doing wrong here that results in both of these attempting failing? What do I need to do differently for them to function properly?
2) Is there a better method to get a friendly claims user name without string parsing?
sigh Somehow the i: at the beginning of the claims string was being lopped off.
Should have read i:0e.t|saml provider|first.last#domain.local. Once that was added back, everything started functioning properly.

IPrincipal.IsInRole() only works when I truncate the role names - why?

I have an application that relies heavily on authorization of users. Within it, I am using IPrincipal.IsInRole() to check whether users are in the correct groups:
IPrincipal principal = Thread.CurrentPrincipal;
bool inRole = principal.IsInRole("mydomainname\some role with a long name");
This works fine for the most part, but fails (returns an incorrect result) if the principal is an instance of a WindowsPrincipal. I have found that to make it work correctly, I have to truncate the name of the role that I pass in to be 32 characters long (including the domain name and the \):
IPrincipal principal = Thread.CurrentPrincipal; // <- returns a WindowsPrincipal
bool inRole = principal.IsInRole("mydomainname\some role with a lo");
Truncating the role name then works correctly. Why? Is this a bug/feature/documented issue? I have an inkling that it may be related to Win2000 domains, but cannot find any info on it.
Some extra info:
This is a problem because the application can be configured to use either active directory or "custom" for its authorization ("custom" being any authorization provider that supports an interface - could be SQL-based, file-based, etc..). When custom is configured, the roles most likely do not need truncating and so I don't want to have to deal with this special case in my code. Additionally, I have another part of the application that uses classes in the System.DirectoryServices.AccountManagement namespace to look up groups memberships. This requires the full role name and does not work if they are truncated.
After much trial and error, I have figured out what is going on.
When a group is created in Active Directory, it is given two names:
It seems to be that WindowsPrincipal uses the pre-Windows 2000 group name when IsInRole is called.
After searching extensively, this does not seem to be documented anywhere. The closest I got was this speculative answer to a similar question here on SO.
In my case, the groups I was querying against on the domain had a long name, but a truncated pre-Windows 2000 name (truncated to 32 characters for some reason). Passing in the long name does not work as it was checking against the wrong group name.

Categories

Resources