creating home directories with permissions in activedirectory using com

Wed, Jul 1, 2009

Creating users in ActiveDirectory is pretty straightforward. You connect to the domain controller:

ADsOpenObject(m_connectionString,
             (LPCWSTR)m_username, (LPCWSTR)m_password,
             ADS_SECURE_AUTHENTICATION,
             IID_IADsContainer,
             (void)&m_pContainer);
create the user, getting an IDispatch interface back:
m_pContainer->Create(CComBSTR(“user”), CComBSTR(wcCN), &pDispatch);
query the IDispatch interface to get the IADsUser interface:
pDispatch->QueryInterface(IID_IADsUser, (void)&pUser);
and use it to set the various properties of the new user object:
BSTR prop = SysAllocString(L”samAccountName”);
var.vt = VT_BSTR;
var.bstrVal = SysAllocString(pUserDetails->GetUsername());
m_hr = pUser->Put(prop, var);
SysFreeString(prop);
VariantClear(&var);
if (!SUCCEEDED(m_hr)) {
  pUser->Release();
  return false;
}
All fine and well but what about the user’s home directory? ADSI has no option for accessing the filesystem. You can connect to the lanmanserver on a DC but it only gives access to a limited set of resources, enumerated here. None of them has anything to do with directories. You can create file shares from existing files but not new files. There is just no way, using ADSI, to access the filesystem. So what to do? You have to use COM to load the filesystem scripting subsystem:
CComPtr pDispatchScripting;
CComDispatchDriver scriptingDispatchDriver(pDispatchScripting);
scriptingDispatchDriver.CoCreateInstance(L”Scripting.FileSystemObject”);
then use that to create the directory:
CComVariant funcArg(pDirDetails->GetName()), createFolderRetValue(false);
scriptingDispatchDriver.Invoke1(L”CreateFolder”, &funcArg, &createFolderRetValue);
where pDirDetails()->GetName() is something is “\SERVER\Share\adsiuser”. This works ok to create the directory and you can run this code outside the AD domain, as long as the client can resolve \SERVER. A directory without permissions is no use though, so that’s the next job, for which we need the security subsystem:
CComPtr pDispatchSecurity;
pDispatchSecurity.CoCreateInstance(L”ADsSecurityUtility”);
CComDispatchDriver securityDispatchDriver(pDispatchSecurity);
config the call with its arguments:
CComVariant getSecDescArg1(pDirDetails->GetName());
// As opposed to FileShare or Registry
CComVariant getSecDescArg2(ADS_PATH_FILE);
// tells it to return IADsSecurityDescriptor
CComVariant getSecDescArg3(ADS_SD_FORMAT_IID); and then step back in horror as it fails repeatedly to get the security descriptor for the remote folder. What I didn’t realise at first was InvokeN() INVERTS the argument list!
CComVariant varArgs[3] = {getSecDescArg3, getSecDescArg2, getSecDescArg1};
CComVariant getSecDescriptorRetValue;
securityDispatchDriver.InvokeN(L”GetSecurityDescriptor”, varArgs, 3,
  &getSecDescriptorRetValue);
what follows takes me back to my driver days. You have to cast the return value to a pointer to an IADsSecurityDescriptor interface:
IADsSecurityDescriptor* pSecurityDescriptor = ((IADsSecurityDescriptor)
  (((tagVARIANT)(&getSecDescriptorRetValue))).pdispVal);
don’t you just love casting pointers? I do! So let’s now make the user the owner of the directory:
pSecurityDescriptor->put_Owner(CComBSTR(pDirDetails->GetOwner()));
and let’s also get the ACL for the directory:
IADsAccessControlList pACL;
pSecurityDescriptor->get_DiscretionaryAcl((IDispatch**)&pACL);
and add an ACE for the user, giving them full rights to the directory:
IADsAccessControlEntry *pACE = NULL;
CoCreateInstance(CLSID_AccessControlEntry, NULL, CLSCTX_INPROC_SERVER,
  IID_IADsAccessControlEntry, (void **)&pACE);
pACE->put_AccessMask(-1);
pACE->put_AceType(ADS_ACETYPE_ACCESS_ALLOWED);
pACE->put_AceFlags(ADS_ACEFLAG_INHERIT_ACE);
pACE->put_Trustee(CComBSTR(pDirDetails->GetOwner()));
add that to the ACL:
pACL->AddAce(pACE);
update the ACL for the directory with the new ACE:
pSecurityDescriptor->put_DiscretionaryAcl(pACL);
and finish by updating the security descriptor for the directory, remembering to invert the arguments to InvokeN():
CComVariant setSecDescArg1(pDirDetails->GetName());
CComVariant setSecDescArg2(1);
CComVariant setSecDescArg3(pSecurityDescriptor);
CComVariant setSecDescArg4(1);
CComVariant varArgs2[4] = {setSecDescArg4, setSecDescArg3,
  setSecDescArg2, setSecDescArg1};
CComVariant setSecDescriptorRetValue;
securityDispatchDriver.InvokeN(L”SetSecurityDescriptor”, varArgs2, 4,
  &setSecDescriptorRetValue);
The trouble really begins when you try to set the owner of the directory. Whereas in ADSI, everything works remotely, i.e. you make a call to the DC and the work is carried out by the DC, on the DC, when you use the local system to load the security subsystem, everything is done on the local system and the results sent to the remote server. It’s daft. It’s like calling a web service with the result you want. You’re in effect saying to the web service, “here’s a reply I prepared earlier, just return that”. It worked fine when I tested using BUILTIN\Administrator but when I tried it with a user I’d created using ADSI, the owner was set to something like:
S-1-5-xx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx
which is a Windows SID and completely useless. This was the result of setting the owner to SERVER\adsiuser. It worked for BUILTIN\Administrator as that account is common to all machines, so the DC could resolve it. Well, what I mean is, it used it as is. That’s what came from the client machine. The client machine resolved it to a SID that the DC could resolve to BUILTIN\Administrator. It had no idea what the SID resolved to for the garbage the client sent for SERVER\adsiuser. Because the client was doing all the work of resolving the user, instead of being a proper service client and letting the DC do the work, it was trying to resolve a user it couldn’t find and sending garbage as a result. The solution? Move the client inside the AD domain. I really didn’t want to do that as I wanted to be able to run the provisioning from anywhere but this was the blocker. There’s just no way to set the owner from outside the domain.

However, a side effect of running inside the domain is you don’t have to mount the remote server before creating directories on it:

NETRESOURCE nr;
memset(&nr, 0, sizeof (NETRESOURCE));
nr.dwType = RESOURCETYPE_ANY;
nr.lpLocalName = L”X:“;
nr.lpRemoteName = L”\\server\share”;
nr.lpProvider = NULL;
DWORD dwFlags = CONNECT_UPDATE_PROFILE;
DWORD dwRetVal = WNetAddConnection2(&nr, L”password”, L”domain\user”, dwFlags);
When I started looking into this I’d never heard of lanmanserver, Scripting.FileSystemObject, WScript.Network or ADsSecurityUtility. I just thought you could use ADSI to create directories. In a way it’s similar to Novel’s eDirectory, where you can create user accounts using LDAP (ADSI uses LDAP under the covers too) but you have to use NDAP to create the directories with permissions. NDAP lets the remote system do the work though, unlike ADsSecurityUtility, which tries to do everything on the client before sending the call to the server. That’s why you have to run inside the domain, to let ADsSecurityUtility resolve users. I did try this format for specifying the user:
LDAP://cn=adsiuser,ou=students,ou=sm,ou=siva,dc=green,dc=local
but even that didn’t work. ADsSecurityUtility was adamant it was going to do all the work, whether it was capable of doing so or not. The server could process whatever garbage it produced as far as it was concerned.

So the journey through AD land was frustrating but technically and intellectually stimulating. Along the way I met the BadlyNamedService (FileServiceOperations, which can only start/stop the FileService, it has no filesystem capabilities like creating directories),  InversionOfArguments Monster which seems to suffer from OCD and the FatClient that can’t delegate to the callee when it fails to work. The road has been designed by Microsoft committees. There is no joined up thinking at all. ADSI for users, local scripting system for directories and a paranoid local security system for remote permissions. Underlying the whole caboodle and keeping it all from falling apart is COM. Thank goodness for COM!

comments powered by Disqus