Getting user group in LDAP using Novell.Directory.Ldap.NETStandard - c#

I have simple service that gets user details using Novell.Directory.Ldap.NETStandard and F# (I can provide transcript for c# if that is necessary, but this part is very similar) and it looks like this:
use connection = new LdapConnection();
connection.Connect(credentials.host, LdapConnection.DefaultPort);
connection.Bind($"{credentials.domain}\{credentials.username}", credentials.password);
match connection.Connected with
| true ->
let schema = connection.FetchSchema((connection.GetSchemaDn()));
let filter = $"(SAMAccountName={credentials.username})"
let searcher = connection.Search(String.Empty, LdapConnection.ScopeBase, filter, null, false);
return (searcher |> Some, String.Empty)
| false ->
raise (Exception())
return (None, $"Cannot connect to domain {credentials.domain} with user {credentials.username}")
Now I cant find information about group that this user is assign to, normally when I use Directory.Service I just add:
directorySearcher.Filter <- sprintf "(SAMAccountName=%s)"credentials.username
To directory searcher and I can filter this information out (as Directory.Service is windows limited i can not use it in this project), but I can not find any information how to use it in Novell.Directory.Ldap.

You have to provide the required attributes (ie. memberOf in order to read user's group) as an array of strings instead of null when calling Search() :
let attrs = [| "SAMAccountName"; "memberOf"; |];
let searcher = connection.Search(searchbase, scope, filter, attrs, false);
You can also pass "*" to get all non-operational attributes.

Related

How can I verify in C# if a folder has "Security" to "Everyone - Full Control: Allow"?

How can I verify in C# if a folder has set "Security" to "Everyone - Full Control: Allow?
Many Thanks!
It should be as easy as
var ctrl = new DirectoryInfo(#"D:\something").GetAccessControl();
var everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
var accessRules = ctrl.GetAccessRules(true, true, typeof(SecurityIdentifier));
var result = accessRules
.Cast<FileSystemAccessRule>()
.Any(rule =>
rule.IdentityReference.Value == everyone.Value && // check everyone
rule.AccessControlType == AccessControlType.Allow && // check allow
rule.FileSystemRights == FileSystemRights.FullControl); // check full control
Note : This is windows only (obviously), and you might need System.IO.FileSystem.AccessControl nuget
Additional Resources
Directory.GetAccessControl Method
Returns the Windows access control list (ACL) for a directory.
SecurityIdentifier Class
Represents a security identifier (SID) and provides marshaling and
comparison operations for SIDs.
WellKnownSidType Enum
Indicates a SID that matches everyone.
DirectoryObjectSecurity.GetAccessRules(Boolean, Boolean, Type) Method
Gets a collection of the access rules associated with the specified
security identifier.

Get Invokable Method from __ComObject

I'm optimizing some code we use to query Active Directory. One of the methods fetches all of the AD users who have changed since a particular update, determined by the uSNCreated property of the Directory Entry. Essentially it's doing the C# equivalent of:
select * from PrincipalSearcher where uSNCreated > somevalue
The code is (more or less):
public IEnumerable<UserPrincipal> GetUpdatedUsers(string samAccountName, long lastUsnChanged)
{
using (var context = new PrincipalContext(ContextType.Domain))
using (var userSearcher = new PrincipalSearcher(new UserPrincipal(context)))
{
var items = userSearcher.FindAll().Cast<UserPrincipal>();
return items.Where(x => GetUsnChanged(x) > lastUsnChanged).ToArray();
}
}
private static long GetUsnChanged(Principal item)
{
var de = item.GetUnderlyingObject() as DirectoryEntry;
if (de == null)
return 0;
if (!de.Properties.Contains("uSNCreated"))
return 0;
var usn = de.Properties["uSNCreated"].Value;
var t = usn.GetType();
var highPart = (int)t.InvokeMember("HighPart", BindingFlags.GetProperty, null, usn, null);
var lowPart = (int)t.InvokeMember("LowPart", BindingFlags.GetProperty, null, usn, null);
return highPart * ((long)uint.MaxValue + 1) + lowPart;
}
Now this code DOES work, but the repeated calls to InvokeMember() are SLOW. What I'd like to do is get a reference to the HighPart and LowPart properties so that I can call them over and over without the overhead of needing to "rediscover" them every time when calling InvokeMember().
I'd though that I could do something along the lines of
static PropertyInfo highProp = highProp
?? t.GetProperty("HighPart", BindingFlags.GetProperty);
highPart = (int)highProp.GetValue(usn);
Unfortnately t.GetProperty() always returns null. Looking at the results returned by GetProperties(), GetMethods() and GetMembers(), there doesn't seem to be a visible "HighPart" or "LowPart" that I can get to, even when using BindingFlags.NonPublic - the __ComObject simply doesn't seem to expose them (even though I can call the using InvokeMember())
Is there a way to solve this, or is it time to admit defeat?
Classes from the System.DirectoryServices.AccountManagement namespace are designed for use in simple cases, e. g. you need to find a user or group. These classes have known perfomance issues. I'd recommend using DirectorySearcher or LdapConnection/SearchRequest. In this case you can filter objects on the server, not on the client which will significantly increase performance and reduce data sent over network. Here is an example of using DirectorySearcher to find all users: Get all users from AD domain
In your case the filter will look like (&(objectClass=user)(uSNCreated>=x+1)) where x is your last usn.
Be aware that you if you track objects with usnCreated attribute you will be getting only users that were created since last usn. To track changes use usnChanged attribute

Searching Active Directory B2C by custom property on User

We are using B2C and storing customer numbers as a Extension field on users. A single user can have one or more customers and they are stored in a comma separated string.
What I am doing now is highly inefficient:
1. Get all Users
2. Get extension properties on each user
3. Check if they have the desired extension property and if it contains the customer I want.
4. Build a list of the users I want.
Adclient is IActiveDirectoryClient
var users = (await GetAllElementsInPagedCollection(await AdClient.Users.ExecuteAsync())).ToList();
var customersUsers = users.Where(user => user.AccountEnabled.HasValue && user.AccountEnabled.Value).Where(user =>
{
var extendedProperty = ((User) user).GetExtendedProperties().FirstOrDefault(extProp => extProp.Key == customersExtendedProperty.Name).Value?.ToString();
return extendedProperty != null && extendedProperty.Contains(customerId);
}).ToList();
I want to be able to do this in one query to ActiveDirectory using the AdClient. If I try this I get errors that the methods are not supported, which makes sense as I am assuming a query is being built behind the scenes to query Active Directory.
Edit - additional info:
I was able to query Graph API like this:
var authContext = await ActiveDirectoryClientFactory.GetAuthenticationContext(AuthConfiguration.Tenant,
AuthConfiguration.GraphUrl, AuthConfiguration.ClientId, AuthConfiguration.ClientSecret);
var url = $"https://graph.windows.net:443/hansaborgb2c.onmicrosoft.com/users?api-version=1.6&$filter={customersExtendedProperty.Name} eq '{customerId}'";
var users = await _graphApiHttpService.GetAll<User>(url, authContext.AccessToken);
However, in my example I need to use substringof to filter, but this is not supported by Azure Graph API.
I am not using that library, but we are doing a very similar search using the Graph API. I have constructed a filter that will look for users that match two extension attribute values I am looking for. The filter looks like this:
var filter = $"$filter={idpExtensionAttribute} eq '{userType.ToString()}' and {emailExtensionAttribute} eq '{emailAddress}'";
We have also used REST calls via PowerShell to the Graph API that will return the desired users. The URI with the associated filter looks like this:
https://graph.windows.net/$AzureADDomain/users?`$filter=extension_d2fbadd878984184ad5eab619d33d016_idp eq '$idp' and extension_d2fbadd878984184ad5eab619d33d016_email eq '$email'&api-version=1.6
Both of these options will return any users that match the filter criteria.
I would use normal DirectorySearcher Class from System.DirectoryServices
private void Search()
{
// GetDefaultDomain as start point is optional, you can also pass a specific
// root object like new DirectoryEntry ("LDAP://OU=myOrganisation,DC=myCompany,DC=com");
// not sure if GetDefaultDomain() works in B2C though :(
var results = FindUser("extPropName", "ValueYouAreLookingFor", GetDefaultDomain());
foreach (SearchResult sr in results)
{
// query the other properties you want for example Accountname
Console.WriteLine(sr.Properties["sAMAccountName"][0].ToString());
}
Console.ReadKey();
}
private DirectoryEntry GetDefaultDomain()
{ // Find the default domain
using (var dom = new DirectoryEntry("LDAP://rootDSE"))
{
return new DirectoryEntry("LDAP://" + dom.Properties["defaultNamingContext"][0].ToString());
}
}
private SearchResultCollection FindUser(string extPropName, string searchValue, DirectoryEntry startNode)
{
using (DirectorySearcher dsSearcher = new DirectorySearcher(startNode))
{
dsSearcher.Filter = $"(&(objectClass=user)({extPropName}={searchValue}))";
return dsSearcher.FindAll();
}
}
I came across this post looking to retrieve all users with a specific custom attribute value. Here's the implementation I ended up with:
var filter = $"{userOrganizationName} eq '{organizationName}'";
// Get all users (one page)
var result = await graphClient.Users
.Request()
.Filter(filter)
.Select($"id,surname,givenName,mail,{userOrganizationName}")
.GetAsync();
Where userOrganizationName is the b2c_extension full attribute name.

Case-Sensitive LDAP Queries

I am doing an LDAP query with DirectoryEntry/DirectorySearcher to authenticate a user in Active Directory via a C# web app like so (the ConnectionString property is just equivalent to LDAP://server.domain):
internal bool AuthenticateUser(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return false;
var entry = new DirectoryEntry(this.ConnectionString, username, password);
var searcher = new DirectorySearcher { SearchRoot = entry, Filter = "(objectclass=user)" };
try
{
var result = searcher.FindOne();
return true; //connection to AD succeeded, authentication was successful
}
catch (DirectoryServicesCOMException)
{
return false; //impersonating the user failed
}
}
These queries are all hitting an SBS server which, when you create a new user, appears to use uppercase values for the pre-Windows 2000 (i.e. NetBIOS) name. So, if I add a new user called "Test User", the username might be "tuser" but the NetBIOS name it specifies is "TUser". When a user puts in a user/pass that hits this method, "tuser" fails to be authenticated whereas "TUser" succeeds.
My question is whether it is possible to modify this so usernames don't have to be case-sensitive?
The attribute definition in the schema defines which characters can be used in an attribute value for the attribute being defined. The matching rule(s) - also in the attribute definition in the schema - determine how attribute values are compared for equality, substring, ordering, and so forth. The matching rule(s) determine the "case-sensitivity" (although it's really not that simple) of a comparison of attributes.
Matching rules must be used by the server (and clients) when comparing attribute values.
For OpenLDAP there is a syntax to filter values in case-sensitive way.
Two short examples:
(&(ou:caseExactMatch:=cwm)(objectClass=person))
+ will match case-sensitive ou= value of 'cwm'
- will NOT match 'CWM', 'CwM' or 'Cwm'
(&(ou=cwm)(objectClass=person))
+ will match case-insensitive (by default) all ou= values like 'cwm', 'CWM', 'CwM', 'Cwm'
The syntax seems to be:
attr:matchingRule:=value
See "8.3.4 Matching rules" at:
https://access.redhat.com/documentation/en-US/Red_Hat_Directory_Server/8.2/html/Administration_Guide/Finding_Directory_Entries-LDAP_Search_Filters.html#using-matching-rules
Also there are several matchingRule types:
https://ldapwiki.com/wiki/MatchingRule

Get user information from DN

I have two goals with the below code
1) Get a list of Users who below to a specific AD group
2) Get the email/lastname/first name of all the users that belong to that group
If there is a better way to accomplish both please let me know.
I'm able to get the full DN but I'm not sure how to get the remaining data from the full DN, or if there is a better way to pull this info please let me know. below is the code I'm using but it gets error:
The value provided for adsObject does not implement IADs.
when I tried to do a DirectorySearcher using the full DN.
HashSet<string> User_Collection = new HashSet<string>();
SearchResultCollection sResults = null;
DirectoryEntry dEntryhighlevel = new DirectoryEntry("LDAP://CN=Global_Users,OU=Astrix,OU=Clients,OU=Channel,DC=astro,DC=net");
foreach (object dn in dEntryhighlevel.Properties["member"])
{
DirectoryEntry dEntry = new DirectoryEntry(dn);
Console.WriteLine(dn);
DirectorySearcher dSearcher = new DirectorySearcher(dEntry);
//filter just user objects
dSearcher.SearchScope = SearchScope.Base;
//dSearcher.Filter = "(&(objectClass=user)(dn="+dn+")";
dSearcher.PageSize = 1000;
sResults = dSearcher.FindAll();
foreach (SearchResult sResult in sResults)
{
string Last_Name = sResult.Properties["sn"][0].ToString();
string First_Name = sResult.Properties["givenname"][0].ToString();
string Email_Address = sResult.Properties["mail"][0].ToString();
User_Collection.Add(Last_Name + "|" + First_Name + "|" + Email_Address);
}
Speed is important and yes I understand I'm not using HashSet as it's designed.
I always use the System.DirectoryServices.AccountManagement.
One of the first things you will see is this: "Connections speeds are increased by using the Fast Concurrent Bind (FSB) feature when available. Connection caching decreases the number of ports used."
with that being said I did not test your code against this for speed you will have to do that your self but this is Microsoft's new library.
Here is my code example:
// Create the context for the principal object.
PrincipalContext ctx = new PrincipalContext(ContextType.Domain,
"fabrikam",
"DC=fabrikam,DC=com");
// Create an in-memory user object to use as the query example.
GroupPrincipal u = new GroupPrincipal(ctx) {DisplayName = "Your Group Name Here"};
// Set properties on the user principal object.
// Create a PrincipalSearcher object to perform the search.
PrincipalSearcher ps = new PrincipalSearcher {QueryFilter = u};
// Tell the PrincipalSearcher what to search for.
// Run the query. The query locates users
// that match the supplied user principal object.
PrincipalSearchResult<Principal> results = ps.FindAll();
foreach (UserPrincipal principal in ((GroupPrincipal)results.FirstOrDefault()).Members)
{
string email = principal.EmailAddress;
string name = principal.Name;
string surname = principal.Surname;
}
It looks like you're walking the group membership of some group in AD...(guessing this off of the member reference above)
Anyway, you need to decide what sort of API you're looking for. The one you're using now is a bit lower level (though you could go lower if you want :)). Going higher level is an option as the previous answer eludes to.
To shake out the code a bit more (and help with perf as you mentioned that it is impt to you):
Use the same connection you used for the group membership search itself (ie no additional connect/bind)
Do a base search where the base DN is the user DN, the search filter is (objectclass=*) and the attributes are ONLY the attributes you care about (no *)
You can remove page size. Paging is a way to ask for many objects in groups (aka pages) but a base search only returns 1 object so it doesn't actually do anything.
Base search result count should always be 1.
Keep in mind cross-domain issues too. Make sure you test your code with a 2 domain forest where a group of type domain local in domain1 has a member in it from domain 2. That will yield some additional work to get more properties as you need to connect to a DC in the other domain (or a GC if the few properties you care about are all in the GC partial attribute set...)
Also keep in mind security. If you don't have access to these properties for some user in your domain, what does the code do? The code above would fail in a nasty way. :) You might want to handle this more gracefully...
Hope this helps.
~Eric

Categories

Resources