I have a program that will let me manage users on our terminal server that we use to demo our software. I have been trying to improve the performace of adding users to the system (It adds the main account then it adds sub accounts if needed, for example if I had a user of Demo1 and 3 sub users it would create Demo1, Demo1a, Demo1b, and Demo1c.)
private void AddUsers(UserInfo userInfo, InfinityInfo infinityInfo, int subUserStart)
{
using (GroupPrincipal r = GroupPrincipal.FindByIdentity(context, "Remote Desktop Users"))
using (GroupPrincipal u = GroupPrincipal.FindByIdentity(context, "Users"))
for(int i = subUserStart; i < userInfo.SubUsers; ++i)
{
string username = userInfo.Username;
if (i >= 0)
{
username += (char)('a' + i);
}
UserPrincipal user = null;
try
{
if (userInfo.NewPassword == null)
throw new ArgumentNullException("userInfo.NewPassword", "userInfo.NewPassword was null");
if (userInfo.NewPassword == "")
throw new ArgumentOutOfRangeException("userInfo.NewPassword", "userInfo.NewPassword was empty");
user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);
if (user == null)
{
user = new UserPrincipal(context, username, userInfo.NewPassword, true);
user.UserCannotChangePassword = true;
user.PasswordNeverExpires = true;
user.Save();
r.Members.Add(user);
u.Members.Add(user);
}
else
{
user.Enabled = true;
user.SetPassword(userInfo.NewPassword);
}
IADsTSUserEx iad = (IADsTSUserEx)((DirectoryEntry)user.GetUnderlyingObject()).NativeObject;
iad.TerminalServicesInitialProgram = GenerateProgramString(infinityInfo);
iad.TerminalServicesWorkDirectory = Service.Properties.Settings.Default.StartInPath;
iad.ConnectClientDrivesAtLogon = 0;
user.Save();
r.Save();
u.Save();
OperationContext.Current.GetCallbackChannel<IRemoteUserManagerCallback>().FinishedChangingUser(username);
}
catch (Exception e)
{
string errorString = String.Format("Could not Add User:{0} Sub user:{1}", userInfo.Username, i);
try
{
if (user != null)
errorString += "\nSam Name: " + user.SamAccountName;
}
catch { }
OperationContext.Current.GetCallbackChannel<IRemoteUserManagerCallback>().UserException(errorString, e);
}
finally
{
if (user != null)
user.Dispose();
}
}
}
Stepping through the code I have found that user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username); is the expensive call, taking 5-10 seconds per loop.
I found I was having another 5-10 second hit on every GroupPrincipal.FindByIdentity() call too so I moved it out of the loop, the Save() is not expensive. Do you have any other recommendations to help speed this up?
Edit --
The normal case would be the user will exist but it is likely that the sub-user does not exist, but it can exist.
I found a soulution
private void AddUsers(UserInfo userInfo, InfinityInfo infinityInfo, int subUserStart)
{
var userSerach = new UserPrincipal(context);
userSerach.SamAccountName = userInfo.Username + '*';
var ps = new PrincipalSearcher(userSerach);
var pr = ps.FindAll().ToList().Where(a =>
Regex.IsMatch(a.SamAccountName, String.Format(#"{0}\D", userInfo.Username))).ToDictionary(a => a.SamAccountName); // removes results like conversons12 from the search conversions1*
pr.Add(userInfo.Username, Principal.FindByIdentity(context, IdentityType.SamAccountName, userInfo.Username));
using (GroupPrincipal r = GroupPrincipal.FindByIdentity(context, "Remote Desktop Users"))
using (GroupPrincipal u = GroupPrincipal.FindByIdentity(context, "Users"))
for(int i = subUserStart; i < userInfo.SubUsers; ++i)
{
string username = userInfo.Username;
if (i >= 0)
{
username += (char)('a' + i);
}
UserPrincipal user = null;
try
{
if (userInfo.NewPassword == null)
throw new ArgumentNullException("userInfo.NewPassword", "userInfo.NewPassword was null");
if (userInfo.NewPassword == "")
throw new ArgumentOutOfRangeException("userInfo.NewPassword", "userInfo.NewPassword was empty");
if (pr.ContainsKey(username))
{
user = (UserPrincipal)pr[username];
user.Enabled = true;
user.SetPassword(userInfo.NewPassword);
}
else
{
user = new UserPrincipal(context, username, userInfo.NewPassword, true);
user.UserCannotChangePassword = true;
user.PasswordNeverExpires = true;
user.Save();
r.Members.Add(user);
u.Members.Add(user);
r.Save();
u.Save();
}
IADsTSUserEx iad = (IADsTSUserEx)((DirectoryEntry)user.GetUnderlyingObject()).NativeObject;
iad.TerminalServicesInitialProgram = GenerateProgramString(infinityInfo);
iad.TerminalServicesWorkDirectory = Service.Properties.Settings.Default.StartInPath;
iad.ConnectClientDrivesAtLogon = 0;
user.Save();
OperationContext.Current.GetCallbackChannel<IRemoteUserManagerCallback>().FinishedChangingUser(username);
}
finally
{
if (user != null)
{
user.Dispose();
}
}
}
}
It adds a few more seconds on the first user but now its about .5 seconds per user after that. The odd calling of the ps.FindAll().ToList().Where(a =>Regex.IsMatch(...))).ToDictionary(a => a.SamAccountName); is because a principle searcher does not cache results. See my question from a few days ago.
Related
I'm trying to get group list while authenticating user. And still getting 0 results. I unfortunately have no environment for testing, so I cannot debug this code (only via logger). Have no results and no exceptions.
private LdapResponce IsAuthenticated(string ldap, string usr, string pwd, out List<string> groups)
{
List<string> result = new List<string>();
try
{
using (var searcher = new DirectorySearcher(new DirectoryEntry(ldap, usr, pwd)))
{
searcher.Filter = String.Format("(&(objectCategory=group)(member={0}))", usr);
searcher.SearchScope = SearchScope.Subtree;
searcher.PropertiesToLoad.Add("cn");
_loggingService.Info(searcher.FindAll().Count.ToString());// here i'm getting 0
foreach (SearchResult entry in searcher.FindAll())
{
try
{
if (entry.Properties.Contains("cn"))
result.Add(entry.Properties["cn"][0].ToString());
}
catch (NoMatchingPrincipalException pex)
{
continue;
}
catch (Exception pex)
{
continue;
}
}
}
groups = result;
}
catch (DirectoryServicesCOMException cex)
{
groups = new List<string>();
if (cex.ErrorCode == -2147023570) return LdapResponce.WrongPassword;
return LdapResponce.Error;
}
catch (Exception ex)
{
groups = new List<string>();
return LdapResponce.Error;
}
return LdapResponce.Passed;
}
Add this to the top of your program
using System.DirectoryServices.AccountManagement;
Use this function and pass the username and the group you are looking to see if they are in. if the group has a group nested it will look in the nested group to see if the user is in that group too.
public static Boolean fctADIsInGroup(string LSUserName, string LSGroupName)
{
Boolean LBReturn = false;
// set up domain context
PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "Put your domain name here. Right click on My computer and go to properties to see the domain name");
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(ctx, LSUserName);
// find the group in question
GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, LSGroupName);
if (user != null)
{
// check if user is member of that group
if (user.IsMemberOf(group))
{
LBReturn = true;
}
else
{
var LSAllMembers = group.GetMembers(true);
foreach(var LSName in LSAllMembers)
{
string LSGPUserName = LSName.SamAccountName.ToUpper();
if (LSGPUserName == PSUserName.ToUpper())
{
LBReturn = true;
}
}
}
}
return LBReturn;
}
I developed function to delete users (soft deletion), ie stays in the database and does not show in the list. When I enter the login and password of the deleted account, it have to show a message as this login is deleted.
how to achieve this and thanks
user.controller.cs:
[Route("api/Users/Login")]
[ResponseType(typeof(object))]
public async Task<object> accountLogin(string name, string password)
{
string pwd = Encrypt(password);
Users account = new Users();
var query = from c in db.Users
where c.userlogin == name && c.password == pwd
select c;
account = query.FirstOrDefault();
string message = "";
if (account == null)
{
message += "404";
}
else if (account .IsActive == 1)
{
message += "400";
}
else if ((account .IsActive != 0) && (account .IsActive != 1)) // this condition of account deleted -- or account .IsActive == 2
{
message += "500";
}
else
{
message = JsonConvert.SerializeObject(account);
}
return new { a = message };
}
and this login.controller.js:
$scope.login = function () {
$scope.loading = true;
authService.login($scope.userlogin)
.then(function done(response) {
console.log(response);
if (response.a == '400') {
$scope.error = 'account disabled';
$scope.loading = false;
}
else if (response.a == '500') {
$scope.error = 'this login is deleted';
$scope.loading = false;
}
else if (response != '404') {
console.log("bbb");
$location.url('Dashboard');
}
else {
console.log("login or password incorrect");
$scope.error = 'login or password incorrect';
$scope.loading = false;
}
},
function fail(err) {
$scope.error = 'Problem in connextion ! contact administrateur';
console.log(err);
}
);
}
Hi I would like to get and count all the invalid accounts from the method valSAM using my other method GetSAM.
I've managed to use the count property to get the total number of accounts in the database from the GetSAM method. (lines 7- 23, GetSAM) The problem is, I do not know how to replicate that and call/ count the total number of invalid accounts from the valSAM method. (lines 20- 39, valSAM)
I have a hunch that I have to somehow call the invalid accounts to the GetSAM method before I am able to call them as well but I do not know how to implement it. Can anyone please advise me on this?
GetSAM method:
//Get SAMAccount
private static string GetSAM(string ldapAddress, string serviceAccountUserName, string serviceAccountPassword)
{
string ldapPath = "LDAP://" + ldapAddress;
string ldapFilter = "(&(objectclass=user)(objectcategory=person))";
DirectoryEntry directoryEntry = new DirectoryEntry(ldapPath, serviceAccountUserName, serviceAccountPassword);
string readOutput;
List<string> list = new List<string>();
StringBuilder builder = new StringBuilder();
using (DirectorySearcher directorySearcher = new DirectorySearcher(directoryEntry))
{
string samAccountName;
directorySearcher.Filter = ldapFilter;
directorySearcher.SearchScope = SearchScope.Subtree;
directorySearcher.PageSize = 1000;
using (SearchResultCollection searchResultCollection = directorySearcher.FindAll())
{
foreach (SearchResult result in searchResultCollection)
{
samAccountName = result.Properties["sAMAccountName"][0].ToString();
valSAM(samAccountName, ldapAddress, serviceAccountUserName, serviceAccountPassword);
list.Add(samAccountName);
} //end of foreach
// Count all accounts
int totalAccounts = list.Count;
Console.WriteLine("Found " + totalAccounts + " accounts. Query in " + ldapAddress + " has finished.\n");
Console.WriteLine("Press [enter] to continue.\n");
readOutput = Console.ReadLine();
}//SearchResultCollection will be disposed here
}
return readOutput;
}
valSAM method:
//Validate SAMAccount
private static string valSAM(string samAccountName, string ldapAddress, string serviceAccountUserName, string serviceAccountPassword)
{
string ldapPath = "LDAP://" + ldapAddress;
DirectoryEntry directoryEntry = new DirectoryEntry(ldapPath, serviceAccountUserName, serviceAccountPassword);
StringBuilder builder = new StringBuilder();
//create instance fo the directory searcher
DirectorySearcher desearch = new DirectorySearcher(directoryEntry);
//set the search filter
desearch.Filter = "(&(sAMAccountName=" + samAccountName + ")(objectcategory=user))";
//find the first instance
SearchResult results = desearch.FindOne();
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, ldapAddress))
{
//if users are present in database
if (results != null)
{
//Check if account is activated
bool isAccountActived = IsActive(results.GetDirectoryEntry());
//Check if account is expired or locked
bool isAccountLocked = IsAccountLockOrExpired(results.GetDirectoryEntry());
//account is invalid
if ((isAccountActived != true) || (isAccountLocked))
{
builder.Append("User account " + samAccountName + " is invalid. ");
if ((isAccountActived != true) && (isAccountLocked))
{
builder.Append("Account is inactive and locked or expired.").Append('\n'); ;
} else if (isAccountActived != true)
{
builder.Append("Account is inactive.").Append('\n'); ;
}
else if (isAccountLocked)
{
builder.Append("Account is locked or has expired.").Append('\n'); ;
}
else
{
builder.Append("Unknown reason for status. Contact admin for help.").Append('\n'); ;
}
}
//account is valid
if ((isAccountActived) && (isAccountLocked != true))
{
builder.Append("User account " + samAccountName + " is valid.").Append('\n');
}
}
else Console.WriteLine("Nothing found.");
Console.WriteLine(builder);
}
return builder.ToString();
}
Updated valSAM:
//Validate SAMAccount
private static bool valSAM(string samAccountName, string ldapAddress, string serviceAccountUserName, string serviceAccountPassword)
{
string ldapPath = "LDAP://" + ldapAddress;
DirectoryEntry directoryEntry = new DirectoryEntry(ldapPath, serviceAccountUserName, serviceAccountPassword);
StringBuilder builder = new StringBuilder();
bool accountValidation = true;
//create instance fo the directory searcher
DirectorySearcher desearch = new DirectorySearcher(directoryEntry);
//set the search filter
desearch.Filter = "(&(sAMAccountName=" + samAccountName + ")(objectcategory=user))";
//find the first instance
SearchResult results = desearch.FindOne();
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, ldapAddress))
{
//if users are present in database
if (results != null)
{
//Check if account is activated
bool isAccountActived = IsActive(results.GetDirectoryEntry());
//Check if account is expired or locked
bool isAccountLocked = IsAccountLockOrExpired(results.GetDirectoryEntry());
accountValidation = ((isAccountActived != true) || (isAccountLocked));
//account is invalid
if (accountValidation)
{
builder.Append("User account " + samAccountName + " is invalid. ");
if ((isAccountActived != true) && (isAccountLocked))
{
builder.Append("Account is inactive and locked or expired.").Append('\n'); ;
} else if (isAccountActived != true)
{
builder.Append("Account is inactive.").Append('\n'); ;
}
else if (isAccountLocked)
{
builder.Append("Account is locked or has expired.").Append('\n'); ;
}
else
{
builder.Append("Unknown reason for status. Contact admin for help.").Append('\n'); ;
}
return false;
}
//account is valid
if ((isAccountActived) && (isAccountLocked != true))
{
builder.Append("User account " + samAccountName + " is valid.").Append('\n');
return true;
}
}
else Console.WriteLine("Nothing found.");
Console.WriteLine(builder);
Console.ReadLine();
}//end of using
return accountValidation;
}
Thanks a million :)
Update: Now I have a new problem after updating my valSAM- I am unable to print out the accounts when I return the boolean accountValidation instead of builder.ToString().
You are returning the call before you do Console.WriteLine, do something like this:
private static bool valSAM(string samAccountName, string ldapAddress, string serviceAccountUserName, string serviceAccountPassword)
{
string ldapPath = "LDAP://" + ldapAddress;
DirectoryEntry directoryEntry = new DirectoryEntry(ldapPath, serviceAccountUserName, serviceAccountPassword);
StringBuilder builder = new StringBuilder();
bool accountValidation = true;
//create instance fo the directory searcher
DirectorySearcher desearch = new DirectorySearcher(directoryEntry);
//set the search filter
desearch.Filter = "(&(sAMAccountName=" + samAccountName + ")(objectcategory=user))";
//find the first instance
SearchResult results = desearch.FindOne();
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, ldapAddress))
{
//if users are present in database
if (results != null)
{
//Check if account is activated
bool isAccountActived = IsActive(results.GetDirectoryEntry());
//Check if account is expired or locked
bool isAccountLocked = IsAccountLockOrExpired(results.GetDirectoryEntry());
accountValidation = ((isAccountActived != true) || (isAccountLocked));
//account is invalid
if (accountValidation)
{
builder.Append("User account " + samAccountName + " is invalid. ");
if ((isAccountActived != true) && (isAccountLocked))
{
builder.Append("Account is inactive and locked or expired.").Append('\n'); ;
} else if (isAccountActived != true)
{
builder.Append("Account is inactive.").Append('\n'); ;
}
else if (isAccountLocked)
{
builder.Append("Account is locked or has expired.").Append('\n'); ;
}
else
{
builder.Append("Unknown reason for status. Contact admin for help.").Append('\n'); ;
}
accountValidation = false;
}
//account is valid
if ((isAccountActived) && (isAccountLocked != true))
{
builder.Append("User account " + samAccountName + " is valid.").Append('\n');
accountValidation = true;
}
}
else Console.WriteLine("Nothing found.");
Console.WriteLine(builder);
Console.ReadLine();
}//end of using
return accountValidation;
}
So now, you can assign the value and have one return point and can also print the names. As for keeping track of counts in main function you can place valSAM call in
if(valSAM(samAccountName, ldapAddress, serviceAccountUserName, serviceAccountPassword))
{
invalidAccountCount++;
}
And needless to say, you have to initialize invalidAccountCount outside the loop.
Hi,
I have a service hosten in IIS that runnes this code :
DirectoryEntry objADAM = default(DirectoryEntry);
// Binding object.
DirectoryEntry objGroupEntry = default(DirectoryEntry);
// Group Results.
DirectorySearcher objSearchADAM = default(DirectorySearcher);
// Search object.
SearchResultCollection objSearchResults = default(SearchResultCollection);
// Binding path.
ActiveDirectory result = new ActiveDirectory();
ActiveDirectoryItem treeNode;
// Get the AD LDS object.
try
{
if (pathToAD.Length > 0)
objADAM = new DirectoryEntry(pathToAD);
else
objADAM = new DirectoryEntry();
objADAM.RefreshCache();
}
catch (Exception e)
{
throw e;
}
// Get search object, specify filter and scope,
// perform search.
try
{
objSearchADAM = new DirectorySearcher(objADAM);
objSearchADAM.Filter = "(&(objectClass=group))";
objSearchADAM.SearchScope = SearchScope.Subtree;
objSearchResults = objSearchADAM.FindAll();
}
catch (Exception e)
{
throw e;
}
// Enumerate groups
try
{
if (objSearchResults.Count != 0)
{
//SearchResult objResult = default(SearchResult);
foreach (SearchResult objResult in objSearchResults)
{
objGroupEntry = objResult.GetDirectoryEntry();
result.ActiveDirectoryTree.Add(new ActiveDirectoryItem() { Id = objGroupEntry.Guid, ParentId = objGroupEntry.Parent.Guid, AccountName = objGroupEntry.Name, Type = ActiveDirectoryType.Group, PickableNode = false });
foreach (object child in objGroupEntry.Properties["member"])
{
treeNode = new ActiveDirectoryItem();
var path = "LDAP://" + child.ToString().Replace("/", "\\/");
using (var memberEntry = new DirectoryEntry(path))
{
if (memberEntry.SchemaEntry.Name.CompareTo("group") != 0 && memberEntry.Properties.Contains("sAMAccountName") && memberEntry.Properties.Contains("objectSid"))
{
treeNode.Id = Guid.NewGuid();
treeNode.ParentId = objGroupEntry.Guid;
treeNode.AccountName = memberEntry.Properties["sAMAccountName"][0].ToString();
treeNode.Type = ActiveDirectoryType.User;
treeNode.PickableNode = true;
treeNode.FullName = memberEntry.Properties["Name"][0].ToString();
byte[] sidBytes = (byte[])memberEntry.Properties["objectSid"][0];
treeNode.ObjectSid = new System.Security.Principal.SecurityIdentifier(sidBytes, 0).ToString();
result.ActiveDirectoryTree.Add(treeNode);
}
}
}
}
}
else
{
throw new Exception("No groups found");
}
}
catch (Exception e)
{
throw new Exception(e.Message);
}
return result;
This works fine in my dev enviroment but at a customer we get this exception :
The specified directory service attribute or value does not exist
I supose that this could have to do with the rights to the Active Directory?
What account needs ActiveDirectory and what level of rights is needed?
The account running the thread needs to have read rights to AD. All domain accounts have this permission.
To cut a long story short, verify that the value of HttpContext.Current.User.Identity.Name is a domain account.
If the web application is configured to have anonymous access, then most likely it won't be.
I'm using the System.DirectoryServices.AccountManagement namespace classes to manage the membership of several groups. These groups control the population of our print accounting system and some of them are very large. I'm running into a problem removing any user from one of these large groups. I have a test program that illustrates the problem. Note that the group I'm testing is not nested, but user.IsMemberOf() also seems to have the same problem, whereas GetAuthorizationGroups() correctly shows the groups a user is a member of. The group in question has about 81K members, which is more than it should have since Remove() isn't working, and will normally be about 65K or so.
I'd be interested to hear from other people who have had this problem and have resolved it. I've got an open case with Microsoft, but the turn around on the call is slow since the call center is about 17 hours time difference so they don't arrive for work until about an hour before I usually leave for home.
using (var context = new PrincipalContext( ContextType.Domain ))
{
using (var group = GroupPrincipal.FindByIdentity( context, groupName ))
{
using (var user = UserPrincipal.FindByIdentity( context, userName ))
{
if (user != null)
{
var isMember = user.GetAuthorizationGroups()
.Any( g => g.DistinguishedName == group.DistinguishedName );
Console.WriteLine( "1: check for membership returns: {0}", isMember );
if (group.Members.Remove( user ))
{
Console.WriteLine( "user removed successfully" );
group.Save();
}
else
{
// do save in case Remove() is lying to me
group.Save();
Console.WriteLine( "user remove failed" );
var isStillMember = user.GetAuthorizationGroups()
.Any( g => g.DistinguishedName == group.DistinguishedName );
Console.WriteLine( "2: check for membership returns: {0}", isStillMember );
}
}
}
}
}
Turns out this is a bug in the GroupPrincipal.Members.Remove() code in which remove fails for a group with more than 1500 members. This has been fixed in .NET 4.0 Beta 2. I don't know if they have plans to back port the fix into 2.0/3.x.
The work around is to get the underlying DirectoryEntry, then use Invoke to execute the Remove command on the IADsGroup object.
var entry = group.GetUnderlyingObject() as DirectoryEntry;
var userEntry = user.GetUnderlyingObject() as DirectoryEntry;
entry.Invoke( "Remove", new object[] { userEntry.Path } );
This post helped point me in the right direction, just wanted to add some addition info.
It also works binding directly to the group, and you can use it for adding group members.
using (var groupEntry = new DirectoryEntry(groupLdapPath))
{
groupEntry.Invoke("remove", new object[] { memberLdapPath });
groupEntry.Invoke("add", new object[] { memberLdapPath });
}
Also be aware, with the standard 'member' attribute, you use the user or group distinguishedName, but invoke requires the path with LDAP:// prefix, otherwise it throws a vague InnerException:
Exception from HRESULT: 0x80005000
public bool RemoveUserFromGroup(string UserName, string GroupName)
{
bool lResult = false;
if (String.IsNullOrEmpty(UserName) || String.IsNullOrEmpty(GroupName)) return lResult;
try
{
using (DirectoryEntry dirEntry = GetDirectoryEntry())
{
using (DirectoryEntry dirUser = GetUser(UserName))
{
if (dirEntry == null || dirUser == null)
{
return lResult;
}
using (DirectorySearcher deSearch = new DirectorySearcher())
{
deSearch.SearchRoot = dirEntry;
deSearch.Filter = String.Format("(&(objectClass=group) (cn={0}))", GroupName);
deSearch.PageSize = 1000;
SearchResultCollection result = deSearch.FindAll();
bool isAlreadyRemoved = false;
String sDN = dirUser.Path.Replace("LDAP://", String.Empty);
if (result != null && result.Count > 0)
{
for (int i = 0; i < result.Count; i++)
{
using (DirectoryEntry dirGroup = result[i].GetDirectoryEntry())
{
String sGrDN = dirGroup.Path.Replace("LDAP://", String.Empty);
if (dirUser.Properties[Constants.Properties.PROP_MEMBER_OF].Contains(sGrDN))
{
dirGroup.Properties[Constants.Properties.PROP_MEMBER].Remove(sDN);
dirGroup.CommitChanges();
dirGroup.Close();
lResult = true;
isAlreadyRemoved = true;
break;
}
}
if (isAlreadyRemoved)
break;
}
}
}
}
}
}
catch
{
lResult= false;
}
return lResult;
}