Setting file owner throwing InvalidOperation exception - c#

I'm using the code below to change the owner of a file. This code runs in a Windows Service written in C#, version 4.0. It's running on a local server, running Windows Server 2008 R2 Standard, Service Pack 1.
I need to change the owner of a file that is received via FTP to a domain account. I can log into the box and do it manually using Explorer, but when I try and run this via code, I get an InvalidOperation exception. I can change the owner to the Local System account, but not a network account. Any help on this would be greatly appreciated.
I'm working with some bizarre Microsoft Dynamics AX code that handles EDI files. The process requires the owner of the file be a valid DAX user, in this case a Domain User. We have Vendors that send us EDI data via FTP. Our DAX application checks the FTP directory every 10 minutes and processes the files. The process currently fails, because the owner is invalid. So, I've written a service to change the owner of the file when it arrives. However, the code below fails with the exception show below the code example.
var ediFileOwner = new NTAccount("MyDomain", _ediEndpointUserAccount);
var fileSecurity = File.GetAccessControl(fileName);
var everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
fileSecurity.AddAccessRule(new FileSystemAccessRule(everyone, FileSystemRights.FullControl, AccessControlType.Allow));
fileSecurity.AddAccessRule(new FileSystemAccessRule(ediFileOwner, FileSystemRights.TakeOwnership, AccessControlType.Allow));
fileSecurity.SetOwner(ediFileOwner); //Change our owner from to our desired User
File.SetAccessControl(fileName, fileSecurity);
Here is the full Exception:
System.InvalidOperationException: The security identifier is not allowed to be the owner of this object.
at System.Security.AccessControl.NativeObjectSecurity.Persist(String name, SafeHandle handle, AccessControlSections includeSections, Object exceptionContext)
at System.Security.AccessControl.NativeObjectSecurity.Persist(String name, AccessControlSections includeSections, Object exceptionContext)
at System.Security.AccessControl.NativeObjectSecurity.Persist(String name, AccessControlSections includeSections)
at System.Security.AccessControl.FileSystemSecurity.Persist(String fullPath)
at System.IO.File.SetAccessControl(String path, FileSecurity fileSecurity)
UPDATE
If I change the account the service run under to the account I'm trying to change to owner to, I get a different exception.
Unexpected Exception: System.UnauthorizedAccessException: Attempted to
perform an unauthorized operation. at
System.Security.AccessControl.Win32.SetSecurityInfo(ResourceType type,
String name, SafeHandle handle, SecurityInfos securityInformation,
SecurityIdentifier owner, SecurityIdentifier group, GenericAcl sacl,
GenericAcl dacl)

I ended up using some code I found here, http://www.codeproject.com/Articles/10090/A-small-C-Class-for-impersonating-a-User
I had to jump through a few hoops in order to get everything done, but it worked. In order to avoid the errors I was getting, I had to use the Impersonate stuff I found in addition to switching between users throughout.
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
// ...
//Copy the file. This allows our service account to take ownership of the copied file
var tempFileName = Path.Combine(Path.GetDirectoryName(file.FileName), "TEMP_" + file.FileNameOnly);
File.Copy(file.FileName, tempFileName);
var windowID = WindowsIdentity.GetCurrent();
var currUserName = windowID.User.Translate(typeof(NTAccount)).Value;
var splitChar = new[] { '\\' };
//var name = currUserName.Split(splitChar)[1];
//var domain = currUserName.Split(splitChar)[0];
var ediFileOwner = new NTAccount("TricorBraun", _radleyEDIEndpointUserAccount);
//We have to give Access to the service account to delete the original file
var fileSecurity = File.GetAccessControl(file.FileName);
var everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
fileSecurity.AddAccessRule(new FileSystemAccessRule(everyone, FileSystemRights.FullControl, AccessControlType.Allow));
File.SetAccessControl(file.FileName, fileSecurity);
File.Delete(file.FileName);
//We rename our file to get our original file name back
File.Move(tempFileName, file.FileName);
//The following give our desired user permissions to take Ownership of the file.
//We have to do this while running under the service account.
fileSecurity = File.GetAccessControl(file.FileName);
var aosSID = (SecurityIdentifier) ediFileOwner.Translate(typeof(SecurityIdentifier));
fileSecurity.AddAccessRule(new FileSystemAccessRule(aosSID, FileSystemRights.FullControl, AccessControlType.Allow));
File.SetAccessControl(file.FileName, fileSecurity);
//Now we user the Impersonator (http://www.codeproject.com/Articles/10090/A-small-C-Class-for-impersonating-a-User)
//This allows us to manage the file as the Account we wish to change ownership to.
//It makes itself the owner.
using (new Impersonator(_radleyEDIEndpointUserAccount, "MyDomain", "password")) {
_logger.Debug(string.Format("Attempting changing owner to Tricorbraun\\{0}", _radleyEDIEndpointUserAccount));
fileSecurity = File.GetAccessControl(file.FileName);
fileSecurity.SetOwner(ediFileOwner); //Change our owner from LocalAdmin to our chosen DAX User
_logger.Debug(string.Format("Setting owner to Tricorbraun - {0}", _radleyEDIEndpointUserAccount));
File.SetAccessControl(file.FileName, fileSecurity);
}

Related

How to use shared memory between service and User Interface App in c#?

If I write following in service:
mmfKernel = MemoryMappedFile.CreateNew("Global\\myMMF", 1024);
and following in user app:
MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("Global\\myMMF");
I get access denial to global mmf. How to grant access rights to mmfKernel to everyone with all possible rights?
But the other way round,
following in user app after I acquire SeCreateGlobalPrivilege:
mmfKernel = MemoryMappedFile.CreateNew("Global\\myMMF", 1024);
and following in service:
MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("Global\\myMMF");
I get path access denial while creating mmfKernel even though I have already SeCreateGlobalPrivilege.
How to create it as normal user but not admin?
If they run under different user accounts, and you think memory mapped files is a good IPC for your use case, you should use another CreateNew version, which accepts MemoryMappedFileSecurity argument with the permissions.
Here’s how to creates an access control list which gives full control permission to everyone:
var sid = new SecurityIdentifier( WellKnownSidType.WorldSid, null );
var ace = new AccessRule<MemoryMappedFileRights>( sid,
MemoryMappedFileRights.FullControl, AccessControlType.Allow );
var acl = new MemoryMappedFileSecurity();
acl.AddAccessRule( ace );
Then pass that acl object to MemoryMappedFile.CreateNew method.
Consider which permissions you actually need. If your desktop process only needs read access to the shared file, change the access rule accordingly. Also it’s a good idea to use more specific trustee instead of everyone, maybe AuthenticatedUserSid (=“Authenticated users”) in place of WorldSid (=“Everyone”), maybe a specific user account or security group.

Modifying an existing access control entry

I had made a logic error in the FileSystemRights interpretation which was causing it to always applied Read permission no matter what else was entered.
I'm making a ps cmdlet which is meant to be fed a list of username and modify the permissions for a folder of the same name as the user. From my testing this script will create the new special acl entry for the user for an allow or deny entry however it will not modify the entry if it already exists. I.e. if a user has read access already and I attempt to grant write access it does not change the entry. I am not sure how I would go about modifying the existing permission without completely removing the old permissions.
DirectoryInfo diDirInfo = new DirectoryInfo(FolderName);
DirectorySecurity dsDirSecurity = diDirInfo.GetAccessControl();
//These just interpet the objects for the rights and the allow/deny entries from the command line
FileSystemRights FSR = genFSR();
AccessControlType ACT = genAct();
dsDirSecurity.AddAccessRule(new FileSystemAccessRule(UserName, FSR, ACT));
diDirInfo.SetAccessControl(dsDirSecurity);
I tried ModifyAccessRule and got the same behavior.
FileSystemAccessRule fsaRule = new FileSystemAccessRule(UserName, FSR, ACT);
dsDirSecurity.ModifyAccessRule(AccessControlModification.Add, fsaRule, out modified);
Use ModifyAccessRule instead of AddAccessRule.
See http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.objectsecurity.modifyaccessrule.aspx

How can I remove ACL from folder that owned by non-existing user

I am developing a C# application.
I need to change the ACLs on a folder, to do so I am running my program as elevated administrator, and everything works fine.
The problem is that if the user that owns the folder got deleted from the system, then when I try to take ownership on the folder I get unauthorized exception.
This is the code that fails:
using (new PrivilegeEnabler(Process.GetCurrentProcess(), Privilege.TakeOwnership))
{
var directorySecurity = directoryInfo.GetAccessControl();
directorySecurity.SetOwner(WindowsIdentity.GetCurrent().User);
Directory.SetAccessControl(directoryInfo.FullName, directorySecurity);
}
The exception occurs on the line: directoryInfo.GetAccessControl();
PrivilegeEnabler is a class defined in Process Privileges , and it's used to take ownership on the file.
I found a solution.
You need to set the owner, by creating a new access control (without calling to GetAccessControl) and setting the owner to the current process.
and then you can do whatever you want with the file.
using (new PrivilegeEnabler(Process.GetCurrentProcess(), Privilege.TakeOwnership))
{
//create empty directory security
var directorySecurity = new DirectorySecurity();
//set the directory owner to current user
directorySecurity.SetOwner(WindowsIdentity.GetCurrent().User);
//set the access control
Directory.SetAccessControl(directoryInfo.FullName, directorySecurity);
}

read permission denied using NTFS when I denied write permission

Using the below code I denied write permission for a user, even when I checked security tab only write permission is denied, but I'm not able to access the folder for reading.
ADsSecurity objADsSec;
SecurityDescriptor objSecDes;
AccessControlList objDAcl;
AccessControlEntry objAce1;
AccessControlEntry objAce2;
Object objSIdHex;
ADsSID objSId;
objADsSec = new ADsSecurityClass();
objSecDes = (SecurityDescriptor)(objADsSec.GetSecurityDescriptor("FILE://" + vPath));
objDAcl = (AccessControlList)objSecDes.DiscretionaryAcl;
objSId = new ADsSIDClass();
objSId.SetAs((int)ADSSECURITYLib.ADS_SID_FORMAT.ADS_SID_SAM, UserName.ToString());
objSIdHex = objSId.GetAs((int)ADSSECURITYLib.ADS_SID_FORMAT.ADS_SID_SDDL);
objAce2 = new AccessControlEntryClass();
objAce2.Trustee = (objSIdHex).ToString();
objAce2.AccessMask = (int)ActiveDs.ADS_RIGHTS_ENUM.ADS_RIGHT_GENERIC_WRITE;
objAce2.AceType = (int)ActiveDs.ADS_ACETYPE_ENUM.ADS_ACETYPE_ACCESS_DENIED;
objAce2.AceFlags = (int)ActiveDs.ADS_ACEFLAG_ENUM.ADS_ACEFLAG_INHERIT_ACE | 1;
objDAcl.AddAce(objAce2);
objSecDes.DiscretionaryAcl = objDAcl;
// Set permissions on the NTFS file system folder.
objADsSec.SetSecurityDescriptor(objSecDes, "FILE://" + vPath);
You're not showing objAce1
You need to order Deny ACE entries before Grant ACE entries.
Try swapping the order of entries in the ACL.
Thus, the DACL's list of ACEs should be appropriately ordered. The standard (canonical) ordering is to first place explicit denies, then explicit allows, general (group) denies, and group allows. If the canonical ordering isn't used, unanticipated allows or denies may occur
From Understanding Windows File And Registry Permissions

How can I convert from a SID to an account name in C#

I have a C# application that scans a directory and gathers some information. I would like to display the account name for each file. I can do this on the local system by getting the SID for the FileInfo object, and then doing:
string GetNameFromSID( SecurityIdentifier sid )
{
NTAccount ntAccount = (NTAccount)sid.Translate( typeof( NTAccount ) );
return ntAccount.ToString();
}
However, this does not work for files on a network, presumably because the Translate() function only works with local user accounts. I thought maybe I could do an LDAP lookup on the SID, so I tried the following:
string GetNameFromSID( SecurityIdentifier sid )
{
string str = "LDAP://<SID=" + sid.Value + ">";
DirectoryEntry dirEntry = new DirectoryEntry( str );
return dirEntry.Name;
}
This seems like it will work, in that the access to "dirEntry.Name" hangs for a few seconds, as if it is going off and querying the network, but then it throws a System.Runtime.InteropServices.COMException
Does anyone know how I can get the account name of an arbitrary file or SID? I don't know much about networking or LDAP or anything. There's a class called DirectorySearcher that maybe I'm supposed to use, but it wants a domain name, and I don't know how to get that either - all I have is the path to the directory I'm scanning.
See here for a good answer:
The best way to resolve display username by SID?
The gist of it is this bit:
string sid="S-1-5-21-789336058-507921405-854245398-9938";
string account = new System.Security.Principal.SecurityIdentifier(sid).Translate(typeof(System.Security.Principal.NTAccount)).ToString();
This approach works for me for non-local SID's over the active directory.
The SecurityReference object's Translate method does work on non-local SIDs but only for domain accounts. For accounts local to another machine or in a non-domain setup you would need to PInvoke the function LookupAccountSid specifying the specific machine name on which the look up needs to be performed.
System.DirectoryServices.AccountManagement.UserPrincipal class (msdn link) has a static function FindByIdentity to convert an SID to a User object. It should be able to work both against the local machine or an LDAP/Active Directory server. I have only used it against active directory.
Here is an example that I have used in IIS:
// Set the search context to a specific domain in active directory
var searchContext = new PrincipalContext(ContextType.Domain, "YOURDOMAIN", "OU=SomeOU,DC=YourCompany,DC=com");
// get the currently logged in user from IIS
MembershipUser aspUser = Membership.GetUser();
// get the SID of the user (stored in the SecurityIdentifier class)
var sid = aspUser.ProviderUserKey as System.Security.Principal.SecurityIdentifier;
// get the ActiveDirectory user object using the SID (sid.Value returns the SID in string form)
var adUser = UserPrincipal.FindByIdentity(searchContext, IdentityType.Sid, sid.Value);
// do stuff to user, look up group membership, etc.
In C#, get the user SID and assign it to a string variable through:
string strUser = System.Security.Principal.WindowsIdentity.GetCurrent().User.ToString();
You will need to use string because the ability to resolve to the UserName supports string. In other words, using var varUser will result in a namespace error.
string strUserName = new System.Security.Principal.SecurityIdentifier(strUser).Translate(typeof(System.Security.Principal.NTAccount)).ToString();
You can also get account name of special accounts like "Everyone" with code like this that will work regardless of user's language settings:
SecurityIdentifier everyoneSid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
string everyone = everyoneSid.Translate(typeof(System.Security.Principal.NTAccount)).ToString();
Ooh, then it's possible that the LDAP call is not working because you might not be in an Active Directory environment. If this is the case, then each of your machines is responsible for its own identity store. And your first code sample is not working across the network because the machine on which you are executing your code does not know how to resolve the SID that only makes sense on the remote machine.
You really should check if your machines are a part of an Active Directory. You would know this during the logon process. Or you can check by right clicking on "My Computer", select "Properties", the "Computer Name" tab, then see if your computer is part of a domain.
Great. I cribbed some LookupAccountSid() code from here:
http://www.pinvoke.net/default.aspx/advapi32.LookupAccountSid
And that worked, though I had to provide the host name myself. In the case of a UNC path I can just take the first component of it. When it's a mapped drive, I use this code to convert the path to a UNC one:
http://www.wiredprairie.us/blog/index.php/archives/22
It seems to work, so that's how I'll do it, unless someone comes up with a situation in which the first component of a UNC path isn't the host name...
Thank you all for your help.
This one is a stumper. You are in an Active Directory environment right? Just checking:)
Anyhow, instead of binding with sid.Value,
string str = "LDAP://<SID=" + sid.Value + ">";
I would try converting the SID's byte array to an Octet String and bind with that instead.
There is a sweet example here on page 78. This will get you closer. To be honest, I've not tried binding with a SID before. But I've had success binding with a user's GUID though :)
Good luck and let me know how it goes.
Get the current domain:
System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain();
Get a directory entry from ldap and the domain name:
DirectoryEntry de = new DirectoryEntry(string.Format("LDAP://{0}", domain));
Get the sid from an ActiveDirectoryMembershipProvider ActiveDirectoryMembershipUser:
ActiveDirectoryMembershipUser user = (ActiveDirectoryMembershipUser)Membership.GetUser();
var sid = (SecurityIdentifier)user.ProviderUserKey;
Get the username from the SecurityIdentifier:
(NTAccount)sid.Translate(typeof(NTAccount));
Get directory search done on an activedirectory with the domain directory entry and username:
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = string.Format("(SAMAccountName={0})", username);
search.PropertiesToLoad.Add("Name");
search.PropertiesToLoad.Add("displayName");
search.PropertiesToLoad.Add("company");
search.PropertiesToLoad.Add("homePhone");
search.PropertiesToLoad.Add("mail");
search.PropertiesToLoad.Add("givenName");
search.PropertiesToLoad.Add("lastLogon");
search.PropertiesToLoad.Add("userPrincipalName");
search.PropertiesToLoad.Add("st");
search.PropertiesToLoad.Add("sn");
search.PropertiesToLoad.Add("telephoneNumber");
search.PropertiesToLoad.Add("postalCode");
SearchResult result = search.FindOne();
if (result != null)
{
foreach (string key in result.Properties.PropertyNames)
{
// Each property contains a collection of its own
// that may contain multiple values
foreach (Object propValue in result.Properties[key])
{
outputString += key + " = " + propValue + ".<br/>";
}
}
}
Depending on the data in your active directory, you will get a varied response in the output.
Here is a site that has all the user properties I needed:
For all the Windows developers, the answer is LookupAccountSid
LookupAccountSid(null, Sid, username, userSize, domainName, domainSize, sidType);
I am quite sure you will be able to use the accepted answer from here: Determine the LocalSystem account name using C#
Basically, you can translate an instance of the SecurityIdentifier class to type NTAccount, from which you can get the user name. In code:
using System.Security.Principal;
SecurityIdentifier sid = new SecurityIdentifier("S-1-5-18");
NTAccount acct = (NTAccount)sid.Translate(typeof(NTAccount));
Console.WriteLine(acct.Value);

Categories

Resources