I have a very old project that worked like a charm for many years.
All of a sudden It seems that the following code snippet is only returning false because the "allowedAttributesEffective" property is null, and therefore thinking that the user has no rights to Modify an AD object
public static bool checkWritePermission(DirectoryEntry de) {
de.RefreshCache(new string[] { "allowedAttributesEffective" });
log.write("Write permission == " + (bool)(de.Properties["allowedAttributesEffective"].Value != null), 5);
// Outputs always -> "Write permission == False"
return de.Properties["allowedAttributesEffective"].Value != null;
}
The "original" codes taken years ago from here: How can I check if a user has write rights in Active Directory using C#?
In reality the user that executes this code should have modify permissions on all AD Objects (proven by the fact that I can Modify all objects in question via Active Directory Users and Computers Management Interface)
Any Ideas what the issue could be here?
I have also tried with ldapsearch on linux to see if I can read the property "allowedAttributesEffective" but I have not found a clear answer if that should even be the case as it is not returning anything on any LDAP object
Related
We have a .NET Framework application used for changing user passwords. It uses the System.DirectoryServices.AccountManagement assembly.
Process goes as follows:
A user fills out a simple form (username, old pw, new pw, new pw repeated).
UserPrincipal.FindByIdentity() gets called to find the user
UserPrincipal.ChangePassword() gets called to change the password
Note: Principal context type is set to DOMAIN
Note2: everything works, the issue is with the user folders.
So apparently ChangePassword() creates a user profile in C:\Users folder in the application's machine and I cannot find any information why that happens. I would understand in the context was set to Machine, but in this case it's not.
There are over 6k folders now, one for each user, it's taking up a lot of space and slowing the machine down.
I tried recreating the problem locally and apparently the ChangePassword() creates a TEMP user profile in C:\Users in my computer, but then it disappears.
Code
Configuration
container.RegisterType<PasswordChangeService>(new InjectionFactory(m => new PasswordChangeService(
new PrincipalContext(ContextType.Domain, ConfigurationManager.AppSettings["LdapDomain"], ConfigurationManager.AppSettings["LdapQuery"]),
new LogService(new Persistence.LogDbContext())
)));
Code for finding user, changing password
using (var user = UserPrincipal.FindByIdentity(_principalContext, IdentityType.SamAccountName, passwordChange.Username)) {
if (user == null) {
Fail(passwordChange, "User not found");
}
user.ChangePassword(passwordChange.CurrentPassword, passwordChange.NewPassword);
LogSuccess(passwordChange);
}
Can anyone tell me why the user profiles get created?
How to fix this issue? Is this a configuration or permission problem?
(optional) I've seen examples where UserPrincipal.Save() gets called after, say, ChangePassword(), but it has always worked without it, so in what situation can it be used?
Thank you.
Update
after some unsuccessful searching I found out a few things that might be worth mentioning:
1. Someone from 2009 had the same problem technet link
The last comment "Since all the code snippets utilize the ChangePasword implementation of ADSI (which causes the profile generation) the simplest way to accomplish this programmatically would be to not use ADSI but System.DirectoryServices.Protocols instead and perform an attribute modification against unicodePwd".
2. When calling ChangePassword(), user directory was sometimes not created and at at those times Event Viewer showed a couple of errors
"Windows has backed up this user profile. Windows will automatically try to use the backup profile the next time this user logs on."
"Windows cannot find the local profile and is logging you on with a temporary profile. Changes you make to this profile will be lost when you log off."
3. Every "fix" I've seen resulted in fixing the consequence, but not the cause. I still have no idea why the profiles get created, but if the technet comment is legit, I guess I need to use another implementation.
I ended up using an example from SO link and this is my result:
string ldapPath = ConfigurationManager.AppSettings["LdapPath"];
string domainWithUserName = string.Format(ConfigurationManager.AppSettings["LdapDomain"], "\\", passwordChange.Username);
DirectoryEntry directoryEntry = new DirectoryEntry(ldapPath, domainWithUserName, passwordChange.CurrentPassword);
if (directoryEntry != null) {
DirectorySearcher search = new DirectorySearcher(directoryEntry);
search.Filter = "(SAMAccountName=" + passwordChange.Username + ")";
SearchResult result = search.FindOne();
if (result != null) {
DirectoryEntry userEntry = result.GetDirectoryEntry();
if (userEntry == null) {
Fail(passwordChange, "User not found.");
}
userEntry.Invoke("ChangePassword", new object[] { passwordChange.CurrentPassword, passwordChange.NewPassword});
userEntry.CommitChanges();
LogSuccess(passwordChange);
}
}
I still need to narrow down the AD search to a specific OU, etc.
This implementation works, no user profile gets created, Event Viewer shows nothing. It's a shame, really, because I'd rather call a few methods instead of using 15 lines of code that do the same thing.
Following is the code that I have written,
var prevSecInfo = Directory.GetAccessControl(path);
if (Utilities.ShowChangePermissionsWindow(path)) {
var currSecInfo = Directory.GetAccessControl(path);
if (currSecInfo != prevSecInfo)
Utilities.ApplyPermissionsOnSubdirectories(path);
}
So, currently, I am getting the access control info before displaying the permissions window.
Next, I am displaying the permissions window which is actually the Security tab of the file/folder properties window. Changes can be made in permissions once it opens up.
But, in case no changes are made, I don't want to call my ApplyPermissionsOnSubdirectories() method. Hence, I am getting the access control info again in another variable and comparing the previous and the current info.
But, this isn't working. The comparison returns false even when no permissions are changed.
How can I check whether permissions have changed for the given path?
You cannot compare the contents of two reference type objects this way.
if (currSecInfo != prevSecInfo) will always return false, unless they both reference to the same object.
Unfortunately, DirectorySecurity type also does not provide Equals overriden method.
There is a StackOverflow article with some ready-made solutions for comparing permissions:
compare windows file (or folder) permissions
While working on the problem above, I found another solution which looks less complex and shorter is terms of code.
DirectorySecurity securityInfoBefore = Directory.GetAccessControl(path, AccessControlSections.Access);
string aclStrBefore = securityInfoBefore.GetSecurityDescriptorSddlForm(AccessControlSections.Access).ToString();
Here, path is the absolute path to the file/folder.
The aim to get the DirectorySecurity object before the permissions are changed and getting the SecurityDescriptorSddlForm as a string.
Now you can add your code to change the permissions. After the permissions are changed, add the following code,
DirectorySecurity securityInfoAfter = Directory.GetAccessControl(path, AccessControlSections.Access);
string aclStrAfter = securityInfoAfter.GetSecurityDescriptorSddlForm(AccessControlSections.Access).ToString();
The next step would be to compare the before and after strings.
if (aclStrBefore.Equals(aclStrAfter)) {
// permissions have not changed
} else {
// permissions have changed
}
This has helped me so far. Please feel free to add to my answer or correct it if required.
I am working on a configuration tool that needs to set some permissions on a directory based on the identity that a specific web application runs under. The original code simply built the login name based on IIS APPPOOL\<ApppoolName> or based on the well know SID if it was a built in account.
Some similar code failed in a localized environment so I am now trying to get rid of the baked in string.
My solution was this:
public static SecurityIdentifier GetApplicationPoolSid(string name)
{
ApplicationPool pool = Manager.ApplicationPools[name];
if (pool != null)
{
var sddlForm = pool.GetAttributeValue("applicationPoolSid") as string;
if (!String.IsNullOrEmpty(sddlForm))
return new SecurityIdentifier(sddlForm);
}
return null;
}
The problem is that I found "applicationPoolSid" by poking around in the debugger and I cannot find any documentation that says that I am not taking advantage of an undocumented implementation feature that will go away in the future. (This means it won't pass code review.)
I would love to know the approved way of doing what I am looking at here. I would also be happy to know that IIS APPPOOL\<ApppoolName> is guaranteed to never be localized so then we could go back to the old way.
I can find no reference to IIS APPPOOL being localized, which would make sense since neither IIS nor APPPOOL are words in any language, so there's nothing to translate to. Although they are acronyms in English and possibly other languages, I think they are still called IIS and AppPool even in other languages.
You can, by the way, get the SID officially like this:
NTAccount f = new NTAccount(#"IIS AppPool\DefaultAppPool");
SecurityIdentifier s = (SecurityIdentifier)f.Translate(typeof(SecurityIdentifier));
String sidString = s.ToString();
You don't even really need the SID if you're going to pass it to FileSystemAccessRule(), since it has an overload that takes an IdentityReference, which NTAccount is. Just pass it the NTAccount.
This will get you the SID, but does not use the function you asked about and is written in powershell not C#. I am posting this to hopefully assist in getting on the right track regarding the generation using SHA1 rather than actually querying from Windows. Also some systems do not have some SID and user account modules installed so this can be used in those situations.
Function to generate SID based off apppool username (explanation in link at the bottom)
function Get-SID ([String]$winver, [String]$username) {
$sidPrefix = switch ($winver) {
'6.0' { 'S-1-5-80' }
default { 'S-1-5-82' }
}
$userToString = switch ( $sidPrefix ) {
'S-1-5-82' { $username.ToLower() }
default { $username.ToUpper() }
}
$userBytes = [Text.Encoding]::Unicode.GetBytes($userToString)
$sha = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
$hash = $sha.ComputeHash($userBytes)
$sid = $sidPrefix
for ( $i=0; $i -lt 5; $i++ ) {
$sid += '-' + [BitConverter]::ToUInt32($hash, $i*4)
}
return $sid
}
Example usage
Get-SID -winver "6.0" -username "DefaultAppPool"
User 6.0 for Vista or equivalent release, use 6.1/6.2 etc for after that.
Partial credit to Tomas Restrepo from winterdom.com for the powershell, although that script does not actually work because some functions are not included in the code.
and thanks for reading.
I have tried many variations of the following code, and all have returned a null value for the userAccountControl property:
DirectoryEntry de = new DirectoryEntry("LDAP://{my server/domain}");
DirectorySearcher ds= new DirectorySearcher(de);
ds.Filter = "(&(objectClass=user)(objectGUID=" + queryGUID + "))";
ds.PropertiesToLoad.Add("userAccountControl");
foreach (SearchResult sr in ds.FindAll())
{
var userFlags = sr.GetDirectoryEntry().Properties["userAccountControl"].Value;
int flags = (int)userFlags;
bool enabled = (flags & 0x2) == 0x2;
Console.WriteLine("Enabled: {0}", enabled ? "true" : "false");
}
Currently it's filtering using an objectGuid I retrieve from a valid user, converted into the proper form. (Being a test program I don't care about the string concatenation...I'll fix that in production code later.) I could (and have) use(d) other search filter values, including bitwise transitive filters. I've used direct binding versus a directory search. I've written a dozen or more variations of this, and all with the same result: the query succeeds but the userFlags property itself comes back null (is not present).
Since I'm specifically asking for a user class here, I know I'm not inadvertently getting a contact class (which wouldn't have the userAccountControl property). The bitwise operations shown in the code aren't important (I know I can convert to an enum and compare that way). It crashes with a null reference exception before the bitwise operations anyway.
This is running on Windows Server 2008 R2, using .NET 4 (I know of the issue with .NET 4.5 and AD account management). The account running this has both Administrator and Enterprise Administrator privileges. Also, as an aside, I downloaded Softerra's LDAP administrator console, and it as well doesn't show this property as present.
My question is simply why is this value null? It should not be, to my limited knowledge. Did I not set AD up properly in the beginning, perhaps? The search is improperly constructed?
Is your code find user? If it s true can u try that?
//...
var results = ds.FindAll();
foreach (SearchResult sResult in results)
{
var directoryEntry = sResult.GetDirectoryEntry();
using (directoryEntry)
{
bool enabled;
const string attrib = "userAccountControl";
const int ufAccountDisable = 0x0002;
de.RefreshCache(new string[] { attrib });
var flags =(int)de.Properties[attrib].Value;
if (((flags & ufAccountDisable) == ufAccountDisable))
{
enabled = false;
}
else
{
enabled true;
}
}
}
I use this code block. I tried it, after set disable a user in active directory. It must works correctly.
Found it. It turns out that new user attributes are protected and can be accessed only by code running as administrator ("Run As"). I wasn't originally running this code using elevated privilege. Once I did, the attributes appeared. This seems to be similar behavior to querying for tombstoned objects. All I can say is "Doh!" :)
I thought this would be a lot easier, however I'm unable to find a way to determine in my event handler if it is the FIRST check-in of the file..
You see, I'm breaking role inheritance, and selectively inheriting permissions for files in doc libs, yet I wish to do it only once, during the first check-in of the file.
I've tried adding an entry to 'SPListItem.Properties' in the ItemAdded event in order to indicate if the file is new, however the moment I do 'SPListItem.Update()' it vanishes..
I've played with the ItemCheckingIn and ItemCheckedIn events with no success...
My only hope at the moment is adding a SPField to the ContentType to indicate if new file or not, but I really wish to avoid it..
ANY IDEAS????
PLEASE HELP ME!
I would recommend considering not only whether the system account has access, but also if the checked out date of the file is identical to the file's creation date.
public bool IsFirstCheckIn(SPListItem item)
{
// Item not null!
if (item != null)
{
SPSecurity.RunWithElevatedPrivileges(delegate
{
// Open privileged Site
using (SPSite pSite = new SPSite(site.ID))
{
// Open privileged Web
using (SPWeb pWeb = pSite.OpenWeb(web.ID))
{
// Create privileged SharePoint-Objects
SPList pList = GetList(pWeb, list.ID);
SPListItem pItem = GetListItem(pList, item.UniqueId);
// Check the Item
if (pItem == null)
{
// Can't access
return true;
}
else if (pItem.File != null && pItem.File.CheckedOutByUser != null)
{
// If the Item's File and checked out User is set, check if checked out date is equal creation date
return (pItem.File.CheckedOutDate.ToLocalTime() == pItem.File.TimeCreated.ToLocalTime());
}
}
}
});
}
return false;
}
To use the system account, is definitely a good idea, otherwise authorization settings would cause problems. Use the "local time" instead of the "UTC-Time", SharePoint handled the Time Zone while storing!
Seems like, SharePoint used the UTF-FileTime to store the file's creation time but used the Time Zone defined for the SPWeb or for the SPUser to store the file checked out date based on the "local time".
Fortunately the DateTime value does know what it is and can convert it to the same "local time" while calling ToLocalTime(). Strangely it will be still a File-Time while calling ToUniversalTime();
So I got a solution for this.
I'm sorry I can't post code here, I do not have access to internet on my dev machine.
It should be good enough for everyone.
Solution:
During the CheckingIn event I try to access the file using the SHAREPOINT\system("SPSecurity.RunWithElevatedPrivileges()"). During the first check-in the file is new and not accessible to other users besides the uploader.
So if I fail getting the file with SHAREPOINT\system it means its new, and I save its Guid in a dictionary in my EventHandlers class.
Then in the CheckedIn event I simply check if the dictionary contains the Guid of the current item, if it does - FIRST CHECK-IN, if it does not - NOT FIRST CHECK-IN.
Of course after I'm finished with the file I remove the entry from the dictionary.
Hope it helps, if you got any questions you are welcome to ask :)