|
|
-
Recently, I was working on a scenario where I was trying to update the attributes of ADAM objects. If I clear an attribute value which is null and then I call CommitChanges on the object, it gives me an exception :
0x8007200a (The specified directory service attribute or value does not exist)
Here is the code sample which I was trying:
DirectoryEntry de = new DirectoryEntry("LDAP://localhost:50002/CN=TestUser,CN=Roles,CN=Partition1,DC=MyWorld,DC=COM"); de.Properties["otherHomePhone"].Clear(); de.Properties["otherHomePhone"].Add("12345"); de.CommitChanges();
After doing some research on it, I found that this behavior is seen because ADSI doesn’t send the control LDAP_SERVER_PERMISSIVE_MODIFY_OID [ http://msdn.microsoft.com/en-us/library/aa366984(VS.85).aspx ] for ADAM or any third party LDAP servers. This control modifies the default behavior of ldap_modify_ext function which by default doesn’t allow the deletion of an attribute value that is not set.
So If you do not want to fall into this scenario, just check for the null before calling CommitChanges on the object.
DirectoryEntry de = new DirectoryEntry("LDAP://localhost:50002/CN=TestUser,CN=Roles,CN=Partition1,DC=MyWorld,DC=COM"); if (de.Properties["otherHomePhone"].Count > 0) { de.Properties["otherHomePhone"].Clear(); } de.Properties["otherHomePhone"].Add("12345"); de.CommitChanges();
ADAM running on Windows 7/ Vista/ 2008 or 2008 R2 does not exhibit this behavior. However, the workaround will function against all instances of ADAM running on any windows operating system.
|
-
Bruce Payette, one of the original designers of PowerShell, visited us in Charlotte this week. One slide in his PowerPoint deck showed a stack of PowerShell books as long as my arm. Yet, the only book I’ve seen so far that covers the features of PowerShell 2.0 (which will be released when Windows 7 ships) is the Windows PowerShell 2.0 Administrator’s Pocket Consultant, by William R. Stanek. (Microsoft Press, 2009, 462 pp). While the book is certainly pocket-sized if you are a system administrator (-eq “wearing cargo pants”), it is much thicker than most pocket guides. The book gives a good discussion of some of the most exciting features of 2.0, including the integrated debugging environment ise (which you can run in Windows 7 just by typing “powershell_ise” at the command prompt). It also has a fairly complete discussion of how WinRM allows the administrator to invoke scripts on other machines (provided the appropriate execution policy has been set previously on those machines).
There are a few minor issues. The output of scripts is shown in black boxes, which can be hard to read. Perhaps the most serious lack, is information about the ActiveDirectory cmdlets (beyond a mention on page 79), which allow you to query the Active Directory, provision users, and so forth. Given that these cmdlets require Windows Server 2008 Release 2, however, I can understand why many administrators might not have a reason to know about them yet; but, this nicely-bound book I believe will hold up until your legacy servers have been retired.
If you have never programmed before at all, this is NOT the book for you. If you are currently solving problems with the scripting host in your environment, it contains enough material to help you make the transition to object oriented scripting.
|
-
The articles you have seen so far on this blog have been largely demonstrations. When one of our customers asks us how they can do something, such as programmatically creating group policies, we write them some sample code that shows them how to do whatever they have in mind. However, not all problems can be solved by demonstration. We answer the question, “can you do this in Windows” by actually showing you how to do it. Think back to geometry -- you probably have heard at some point that you cannot “square the circle” -- just using a compass and a straightedge, make a square that has the same area as a circle. Yet, how could you prove this?
Sometimes, certain things aren’t possible to achieve in code, given certain constraints, and we won’t be able to provide a sample that will do the impossible. How can we prove that a particular task, given certain constraints, is impossible? One method is reductio ad absurdum, or proof by contradiction. Start by assuming that something is possible, and then show that from this assumption that something results that is clearly impossible -- that, just by inspection, couldn’t possibly happen.
Wouldn’t you like to know who had changed a particular attribute in the Active Directory? We could hold someone accountable for a mistake, or praise them for making a timely change. The Active Directory is an RFC 3377 compliant LDAP directory server (for more information, see the article: Active Directory's LDAP Compliance), but the standard does not define a security model. In the Microsoft implementation, every object is protected by a security descriptor (see: http://msdn.microsoft.com/en-us/library/aa746428(VS.85).aspx). Ultimately, security in the Windows world is role-based -- the highest level of authorization to alter properties of the system is designed to be shared by multiple users, each maintaining their own secret password. If a particular object is protected by being owned only by this highest level, then we can be sure (barring physical compromise of the system or its secrets, of course), that only one of the persons having that role have made the change. Yet, is it possible to go deeper, to know exactly which one of the uses has done the deed?
Suppose that you could actually determine which particular user has made a specific change to an Active Directory object. Suppose that the database that acts as the data store for Active Directory, the Extensible Storage Engine (ESE), actually maintained a record of the security descriptor of the user that made the change -- and that even though there is no “who changed” attribute, there was some record in the DIT file of who that Security Principle was. This is our assumption. From making the assumption, I’m going to demonstrate that a “who changed” attribute is actually impossible without making changes at every domain controller.
Remember that Active Directory is a distributed database. A change made at one domain controller will be propagated, in accord with the enterprises’ replication schedule, to all other domain controllers. Microsoft treats information that travels “across a wire” in a very special way. We disclose it fully. Here (http://msdn.microsoft.com/en-us/openspecifications/default.aspx) is the portal for these Open Specifications, and you can see that the WSPP (Windows Server Protocols) contain a discussion of what domain controllers do when they talk to one another. When you examine the replication protocol, you see the sum total of what is transferred with regard to the information in Active Directory is described by the schema. There is no place for the information to hide. Even if there was some trace in the ESE, that trace couldn’t move from place to place.
You may have seen some programs that could create reports that did distinguish between different users, holding the same security role, changing an attribute in Active Directory. How could such programs possibly work, if there is no “who changed” attribute? How have they managed to square this circle? What they have done is go beyond Active Directory, to augment its functioning. They aren’t accessing something that is there, but hidden -- they are importing an entirely new functionality. This functionality comes with some costs, and some limitations. There are several places where Microsoft provides a way to augment the directory. The Password Change Notification Service (http://technet.microsoft.com/en-us/library/cc720654(WS.10).aspx) is an example. If you install a service on EVERY domain controller in the domain, the service will signal the identity integration server that this has happened. This is an example of a password filter (http://msdn.microsoft.com/en-us/library/ms721882(VS.85).aspx).
In the case of arbitrary LDAP calls, however, there isn’t a predefined hook into the functioning of the wldap32.dll, as we see with password filters. If you wanted to add a “who changed” attribute, you would have to create your own hook. One way to do so would be the Detours library (http://research.microsoft.com/en-us/projects/detours/) from Microsoft Research, which you can incorporate into a program without charge for experimentation, and is licensed for commercial products. It will let you hook into every LDAP call on the machine, and the code in your service will be responsible for updating the “who changed” attribute. Note that this process will need to be installed on every domain controller in the domain. You would need to extend the schema of Active Directory for every attribute that you want to track, so that the information about who changed it can be distributed to the other domain controllers.
A caution, however, in relying upon any “who changed” attribute that a program creates in this fashion. If the way in which the “who changed” service functions is known to the Domain Administrators (if they could use Bing, say, and find the code in a blog post for the “who changed” service), they would be able to impersonate the specific identity of any of the other Domain Administrators in making a change, by changing the attribute directly in the same way in which the service would have changed the attribute. There is no way of guarding the guardians in role-based security, if there is a lack of trust in the final guardians. This fact (http://en.wikipedia.org/wiki/Quis_custodiet_ipsos_custodes) has been known for quite some time.
|
-
Ordinarily, if you create a 64 bit WMI provider to run on a server, you will create a corresponding 32 bit provider as well. In some instances, only a 64 bit WMI provider exists. This creates an interesting dilemma. Consider the scenario where a 32 bit exe running in WOW64 on a 64 bit client is attempting to request instances from the 64 bit provider on the 64 bit server. The server will return a WBEM_E_INVALID_CLASS error back to the client. This is because WMI on the 64 bit server assumes that the 32 bit application is requesting an instance provided by a 32 bit provider on the server. Since there is no 32 bit provider for the class on the 64 bit server, the WBEM_E_INVALID_CLASS error will be returned.
WMI provides a way to avoid this error by allowing the client to provide some context information to the a WMI object instance execution method. The following link provides more detail on how WMI can be used to communicate between the two different architectures.
http://msdn.microsoft.com/en-us/library/aa393067(VS.85).aspx
How about a more practical example. Let’s suppose you have a 64 bit exchange server and you are interested in retrieving information on the Win32_PerfRawData_EXOLEDB_MSEchangeWebMail counter. This counter is only available from the 64 bit provider running on the 64 bit exchange server. You need to support 32 bit as well as 64 bit architectures from a single code base. These applications could be installed on either 64 or 32 bit clients. You decide to write a single 32 bit C# exe to access the counter. The question is, how can you apply the information provided in the link above in your C# code?
Below is a simple C# application that illustrates how to setup a context object to request counter information from the 64 bit provider on the exchange server. If you create a C# console application, you can replace the main method contents with the main method contents below. Be sure to add the System.Management namespace to the references for the project and include the “using System.Management;” in the using namespaces section.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Management;
namespace _32exeon64to64
{
class Program
{
static void Main(string[] args)
{
//
// First, setup the context information to request the 64 bit architecture
// and require the 64 bit provider as described in the MSDN link
//http://msdn.microsoft.com/en-us/library/aa393067(VS.85).aspx
//
ManagementNamedValueCollection mContext = new ManagementNamedValueCollection();
mContext.Add( "__ProviderArchitecture", 64);
mContext.Add( "__RequiredArchitecture", true);
//
// Second, setup create a connectionOption structure with the
// credentials for the connection. In this example, the user ID
// and password are hardcoded. There are a number of ways this information
// can be encrypted and stored so that is it not easily hacked from the binary.
//
ConnectionOptions co = new ConnectionOptions();
co.Username = @"myserver\administrator";
co.Password = @"AdminPassword";
//
// Add our context object to the connection options.
//
co.Context = mContext;
//
// Create a management scope and then connect to the remote
// machine.
//
ManagementScope ms = new ManagementScope(@"\\ExchangeServer\root\CIMV2", co);
ms.Connect();
//
// Create a query object, associate it with a searcher object and our management scope…
//
ObjectQuery oq = new ObjectQuery("SELECT * FROM Win32_PerfRawData_EXOLEDB_MSExchangeWebMail");
ManagementObjectSearcher oSearcher = new ManagementObjectSearcher(ms, oq);
//
// Execute the query.
//
ManagementObjectCollection oReturnCollection = oSearcher.Get();
//
// Loop through the counters.
//
foreach (ManagementObject oReturn in oReturnCollection)
{
//
// display the Counter name
//
Console.WriteLine("Name : " + oReturn["Name"].ToString());
}
}
}
}
|
-
WMI provides two very useful Resultant Set of Policy (RSOP) classes that can be used together to determine the current set of GPOs that are applied to the local machine.
The RSOP_GPO class provides information about GPOs that could be applied to your machine. Instances of this class are divided into three categories:
- Instances that represent applied GPOs
- Instances that represent GPOs that have read-access but not applyGroupPolicy access
- Instances that represent disabled GPOs.
The following MSDN link provides more information about the RSOP_GPO class:
http://msdn.microsoft.com/en-us/library/aa374918(VS.85).aspx.
The RSOP_GPLink WMI class represents the links from a site, domain, organizational unit, or local scope, to one or more GPOs. All the links from the current scope of management (SOM), including those that have been disabled. The RSOP_GPLINK class is documented at the following MSDN link:
http://msdn.microsoft.com/en-us/library/aa374916(VS.85).aspx
Notice the “appliedOrder” property. This property will contain either and integer value that represents the order in which the GPO was applied or the value of 0 which indicates that the GPO was either not linked or not applied.
Using these two classes together, one can determine the actual list of GPOs that are applied to the local machine. The process is very straight forward:
- Perform a WMI query on the RSOP namespace to return only those RSOP_GPLINK objects that have an “appliedOrder” value that is non 0.
- Build a dictionary from the results of the query, building the key name for the GPO.
- Walk the dictionary, querying the namespace for the matching RSOP_GPO class objects.
The following Visual Basic Script (VBS) illustrates how to implement the 3 steps listed above:
strComputer = "."
' Step1: Execute the WMI query to retrieve the matching RSOP_GPLink objects:
' and create the dictionary.
'
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\rsop\computer")
Set colItems = objWMIService.ExecQuery("Select GPO From RSOP_GPLink Where AppliedOrder <> 0")
Set dict = CreateObject("Scripting.Dictionary")
'
' Step 2: Load the dictionary with the query results.
'
For Each objItem in colItems
dict.Add Replace(objItem.GPO, "RSOP_GPO.", ""), Replace(objItem.GPO, "RSOP_GPO.", "")
Next
'
' Step 3: Walk the dictionary, and query the repository for the RSOP_GPO objects that have been
'applied to the local machine and display its properties.
'
For Each vItem In dict.Items
Set colItems = objWMIService.ExecQuery("Select * from RSOP_GPO where " & vItem)
For Each objItem in colItems
Wscript.Echo "Name: " & objItem.Name
Wscript.Echo "GUID Name: " & objItem.GUIDName
Wscript.Echo "ID: " & objItem.ID
Wscript.Echo "Access Denied: " & objItem.AccessDenied
Wscript.Echo "Enabled: " & objItem.Enabled
Wscript.Echo "File System path: " & objItem.FileSystemPath
Wscript.Echo "Filter Allowed: " & objItem.FilterAllowed
Wscript.Echo "Filter ID: " & objItem.FilterId
Wscript.Echo "Version: " & objItem.Version
Wscript.Echo ""
Wscript.Echo "====="
Next
Next
|
-
An "Access Denied" exception is generated migrating users from Windows 2000 or 2003 domain to a Windows 2008 domain using DsAddSidHistory API (http://msdn.microsoft.com/en-us/library/ms675918(VS.85).aspx) and pass "SrcDomainController" & "SrcDomainCreds" as NULL.
In general, you do not want to hard code administrator credentials into code. Not only is this a security risk, because you can figure out what those credentials are even without access to the source code, but this also makes it difficult to maintain your code, since you will have to recompile it whenever a change is made. Many API’s are capable of delegating the credentials of a user that is running the code to the destination domain. However, DsAddSidHistory() does not, by design, delegate the user’s credentials to the destination domain.
If we are binding to target domain using DsBind() before calling DsAddSidHistory() with "SrcDomainController" and "SrcDomainCreds" as NULL even after adhering all the requirements specified in http://msdn.microsoft.com/en-us/library/ms677982(VS.85).aspx DsAddSidHistroy an “Access Denied” error code value of 5.
The correct approach for using DsAddSidHistory(), if we do not want to pass the "SrcDomainController" and "SrcDomainCreds" explicitly would be to use the DsBindWithSpnEx() http://msdn.microsoft.com/en-us/library/ms675963(VS.85).aspx API. We need to also pass the target Domain Controller's SPN in the ServicePrincipalName parameter with the NTDSAPI_BIND_ALLOW_DELEGATION flag in the BindFlags parameter.
Below is a code snippet that can be used to reprodue the "Access Denied" error:
dRet = DsBind(NULL,szTgtDomain,&hds);
if(hds)
{
dRet=DsAddSidHistory(hds,
NULL,
szSrcDomain,
szSrcUser,
NULL, //Passing NULL, since we do not want to hard code the admin
NULL, //credentials in the code
szTgtDomain,
szTgtUser
);
}
The follow code snippet illustrates how to properly setup your program to delegate the credentials of current process by calling the DsBindWithSpnEx API using the Service Principal Name (SPN) of the target domain controller. dRet = DsBindWithSpnEx(L"DestDC.DestDOM.LOCAL", //Destination Domain controller’s FQDN
L"DestDOM.LOCAL", //Destination Domain’s FQDN
NULL, // Use the calling processes credentials
L"HOST/DestDC.DestDOM.LOCAL", //SPN for the destination Domain controller
NTDSAPI_BIND_ALLOW_DELEGATION, //Flag for delegation
&hds // Bind handle to be used with DsAddSidHistory
);
if(hds)
{
dRet=DsAddSidHistory(hds,
NULL,
szSrcDomain,
szSrcUser,
NULL,
NULL,
szTgtDomain,
szTgtUser
);
}
|
-
This happens because If there is no container specified, the principal context class will create a System.DirectoryServices.DirectoryEntry object by binding to builtin CN=Users container to start searching for users. System.DirectoryServices is built on top of ADSI. ADSI by default does an objectclass=* search as part of its normal bind process unless the fastbind flag is specified. if the user performing the search does not have permission to read the attributes of default users contain, the search operation will fail, thus causing “The specified directory service attribute or value does not exist”, exception.
This is also true when searching computer objects using ComputerPrincipal::FindByIdentity and you don’t have read permission on CN=Computer container and have not specified a container in the constructor of System.DirectoryServices.AccountManagemnt.PrincipalContext. The remarks section of the documentation at http://msdn.microsoft.com/en-us/library/system.directoryservices.accountmanagement.principalcontext.principalcontext.aspx explains the rules followed by the PrincipalContext class in selecting a container when one has not been explicitly specified in the constructor.
The right approach is to specify the container where the object resides if you know the name of the container. Alternatively, you can simply specify the domain naming context as the container. The performance in this case will be inferior to specifying the name of the container explicitly, since the search will encompass the entire domain.
The issue can be reproduced by using the following code (Change the name of the domain and searched user to values appropriate for your environment):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices;
namespace TestInvalidCreds
{
class Program
{
static void Main(string[] args)
{
PrincipalContext pc = new PrincipalContext(ContextType.Domain, "dc163608.local");
UserPrincipal up = UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, "InnerUser");
}
}
}
To reproduce the issue deny read permission for a user on CN=Users container for the user that is running the search, and the run the above code in Visual Studio or the compiled program under the above user account. The searched (in this example InnerUser) user can be in any OU.
You will get an exception “The specified directory service attribute or value does not exist”.
Modify the code as below (Change the name of the domain and searched user to values appropriate for your environment):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices;
namespace TestInvalidCreds
{
class Program
{
static void Main(string[] args)
{
PrincipalContext pc = new PrincipalContext(ContextType.Domain, "dc163608.local","dc=dc163608,dc=Local");
UserPrincipal up = UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, "InnerUser");
}
}
}
It should now return the object.
|
-
As anyone who has managed the Active Directory knows, if you delete an Active Directory object, although it marked as tombstoned, all the linked and non-linked attribute values are cleared. It is therefore difficult to reanimate the object because extra steps are required to recreate linked and non-linked attribute values.
In Windows Server 2008 R2, a new feature was introduced called the Active Directory Recycle Bin. If an object is placed in the AD Recycle Bin, reanimating this object is much simpler, because the linked and non-linked attribute values are retained.
You might be quick to say, "Hey, Let's active that feature right now". Well, there are two slight caveats. In order to use this feature, the domain must be in Windows 2008 R2 domain mode. Also, the forest must be in Windows 2008 R2 forest mode. If you have domain controllers in the forest that are not Windows 2008 R2, it will not be possible to make this change. As this feature can spare hours and hours of effort and downtime, you may want to start planning the migration of older servers.
The following steps outline bringing the domain and the forest to the Windows 2008 R2 native modes, and enabling the feature, using PowerShell.
Note: The next steps will cause irreversible changes to your forest and domain. You should make sure that you have backups from which you can perform an authoritative restore.
In order to raise the Forest and Domain Functional mode, we will need to start with installing the ActiveDirectory Module for Windows PowerShell.
- Windows PowerShell from a Windows 2008 R2 domain controller.
(By default, Windows Powershell should already be pinned on the taskbar. Click the icon to launch)

- Import the ActiveDirectory module in the Powershell Console by typing:
Import-Module ActiveDirectory
- Check the Domain Functional mode. We will use the Get-ADDomain cmdlet to do this.
(Get-ADDomain).DomainMode
- If the Domain Functional mode is not set to Windows2008R2Domain, we will raise the domain functionality by using the Set-ADDomainMode cmdlet. Make sure that you replace yourdomain.com with the FQDN of your domain.
Set-ADDomainMode -Identity yourdomain.com -DomainMode Windows2008R2Domain -Confirm:$false
- Verify the Forest Functional mode. Similar to the Get-ADDomain cmdlet, we will use the Get-ADForest cmdlet to determine the Forest Functional mode.
(Get-ADForest).ForestMode
- If the Forest Functional modeis not set to Windows2008R2Forest, we will raise the forest functional mode. Make sure that you replace yourdomain.com with the FQDN of your domain.
Set-ADForestMode -Identity yourdomain.com -ForestMode Windows2008R2Forest -Confirm:$false
After setting the Forest and Domain Functional mode, we now need to enable to AD Recycle Bin feature.
- Retrieve the configuration naming context of your domain. To do this, we will use the Get-ADRootDSE cmdlet and use the ConfigurationNamingContext attribute value. We will need to store it to a variable so that we can programmatically construct the Recycle Bin's distinguished name later.
$cfgNameCtx = (Get-ADRootDSE).ConfigurationNamingContext
- Append the configurationNamingContext value to construct the Recycle Bin's distinguished name. Because the path is very long, we will do this in multiple steps but you can type it one line if you wish.
$recBin = "CN=Recycle Bin Feature,CN=Optional Features,"
$recBin = $recBin + "CN=Directory Service,CN=Windows NT,CN=Services,"
$recBin = $recBin + $cfgNameCtx
- Obtain the Forest name in order to supply it as a parameter to the Enable-ADOptionalFeature cmdlet. We will use the Get-ADDomain cmdlet to do this and store it to a variable.
$target = (Get-ADDomain).Forest
- Use the Enable-ADOptionalFeature and pass the information we stored in the variables as parameters. Note that this is a single line but may wrap to the next line.
Enable-ADOptionalFeature -Identity $recBin -Scope ForestOrConfigurationSet -Target $target -Confirm:$false
The following steps can be used to see how the new feature works with some test objects in an Organizational Unit. We will create an organizational unit and then create a few users and a group and add the users into the newly created group.
- Create an organizational unit container. To do this, we will use the New-ADOrganizationalUnit cmdlet. We will create it in the defaultNamingContext of the domain.
$defNameCtx = (Get-ADRootDSE).DefaultNamingContext
Set-Location ("AD:\" + $defNameCtx)
New-ADOrganizationalUnit -Name "Test Accounts"
- Provision 100 users. We will use the New-ADUser cmdlet to create the user accounts.
Set-Location "ou=Test Accounts"
ForEach ($i in 1..100) { New-ADUser -Name ("User" + $i) }
- Create a global security group that the users will be added to. We will use the New-ADGroup cmdlet to accomplish this.
New-ADGroup -Name GlobalSecGrp -GroupCategory Security -GroupScope Global
- Use the Add-ADGroupMember cmdlet to add all the newly created users but we must first need to retrieve the newly created users into a variable.
$users = Get-ADUser -Filter 'name -like "User*"'
Add-ADGroupMember GlobalSecGrp -Member $users
At this point, the Active Directory Recycle Bin should be enabled. Any directory objects, when deleted, are stored in the Recycle Bin. We will go ahead and delete the Test Accounts OU and delete all its contents.
- Turn off the default setting of “Protected From Accidental Deletion”. We must first turn off this OU setting before we can successfully delete it. We will use the Get-ADOrganizationalUnit cmdlet and Set-ADOrganizationalUnit cmdlet to do this.
Set-Location ("AD:\" + $defNameCtx)
$testAcctOU = Get-ADOrganizationalUnit -Filter 'name -like "Test Accounts"'
$testAcctOU | Set-ADOrganizationalUnit -ProtectedFromAccidentalDeletion $false
- Delete the OU and and its contents. We will use the Remove-ADOrganizationalUnit cmdlet to do this.
$testAcctOU | Remove-ADOrganizationalUnit -Recursive -Confirm:$false
Now that we have deleted the object, we can now demostrate how to restore the object.
- Restore the deleted OU. We will use the Get-ADObject cmdlet to retrieve the deleted OU and the Restore-ADObject cmdlet to restore it.
Set-Location ("AD:\" + $defNameCtx)
$deletedOU = Get-ADObject -Filter 'name -like "Test Acc*"' -IncludeDeletedObjects
$deletedOU | Restore-ADObject
- Restore the security group.
$deletedGrp = Get-ADObject -Filter 'name -like "GlobalSecGrp*"' -IncludeDeletedObjects
$deletedGrp | Restore-ADObject
- Restore the users
$deletedUsers = Get-ADObject -Filter 'name -like "User*" -and isDeleted -eq $true' -IncludeDeletedObjects
$deletedUsers | Restore-ADObject
Launch the Active Directory Users and Computers tool and you should now be able to see all the users that were deleted. Also, if you look at the GlobalSecGrp and inspect its members, you should see all the deleted users as well.
|
-
In my previous post we discussed how to modify a registry based GPO programmatically. In this post we will focus on using the GPMC ( Group Policy Management Console ) object model to find a GPO that contains a specific string in the display name.
The steps for using the GPMC to locate GPOs from C++ are straight forward:
1. Create an IGPM interface to initialize the GPMC dlls. (http://msdn.microsoft.com/en-us/library/aa814148(VS.85).aspx )
2. Use IGPM::GetDomain to initialize a SOM (Scope of Management) object to manage GPOs from a specific domain. (http://msdn.microsoft.com/en-us/library/aa814309(VS.85).aspx). This returns an IGPMDomain object (http://msdn.microsoft.com/en-us/library/aa814189(VS.85).aspx ).
3. Initialize a variant with the portion of the string you wish to match in the Display Name attribute of the GroupPolicyContainer object in the Active Directory.
4. Retrieve a GPMC Constants object from the IGPM interface by calling IGPM::GetConstants(). This method returns an IGPMConstants object (http://msdn.microsoft.com/en-us/library/aa814177(VS.85).aspx ) Using the GPMC constants object to retrieve GPMC constants will make the code much easier to read.
5. Create a Search Criteria object using the IGPMC::CreateSearchCriteria() method. This method returns an IGPMSearchCriteria object (http://msdn.microsoft.com/en-us/library/aa814259(VS.85).aspx ).
6. Using the IGPMConstants::get_SearchPropertyGPODisplayName() object, create a GPMSearchProperty object. This object will contain information that identifies the attribute you are searching for.
7. Using the IGPMConstants::get_SearchOpContains () method to retrieve a GPMSearchOperation object initialized to a wild card contains search.
8. Add the search property object ( GPMSearchProperty created in step 6) along with the search operation object ( GPMSearchOperation created in step 7) to the search criteria object ( created in step 5 ) with the variant ( initialized in step 3 ) using the IGPMSearchCriteria::Add method (http://msdn.microsoft.com/en-us/library/aa814260(VS.85).aspx )
9. Execute the search using the SOM domain object created in step 2 by calling the IGPMDomain::SearchForGPOs( ) method passing in the IGPMSearchCriteria object initialized in step 8. You will be given a IGPMGPOCollection object (http://msdn.microsoft.com/en-us/library/aa814200(VS.85).aspx ) containing the results of the search. The collection object will contain a set of IGPMGPO objects returned as variants containing IDispatch interfaces.
Let’s take a look at each step in a C++ code block.
//
// Locate a GPO with contains match and modify it
//
{
//
IGPM *pGPM = NULL; // Initial GPMC interface.
// Used to retrieve other GPO objects.
IGPMDomain *pDom = NULL; // Returned by pGPM, limits items
// to a specific domain.
IGPMGPO *pGPO = NULL; // The GPMC interface that
// represents a single GPO
IGPMConstants *pGPMConstants = NULL; // Used to help retrieve
// GPMC constants easily
IGPMSearchCriteria *pIGPMSearch = NULL; // Defines a search
// criteria to locate
// GPOs.
IGPMGPOCollection *pGPOCollection = NULL; // used to search for
// a GPO, this will
// contain the results
// of the search.
//
// To use _bstr_t classes must use the comutil library and
// and header files
//
_bstr_t bErrorStr("");
//
// Retrieve FQDN
//
wchar_t strFQDN[255], strDispName[255];
fflush( stdin );
printf("Enter the FQDN of the domain to create the GPO in: ");
_getws( strFQDN );
fflush(stdin);
//
// Retrieve Display Name
//
printf("Enter Display name for GPO to Match: ");
_getws( strDispName );
fflush(stdin);
//
// Step 1: Create and initialize the GPMC objects.
// Use the GPMC to create SOM and locate GPO
// To use this interface the GPMC components must be present
// on the client.
//
_bstr_t bstrFQDN(strFQDN);
hr = CoCreateInstance(CLSID_GPM, NULL,
CLSCTX_INPROC_SERVER,
IID_IGPM , (LPVOID*)&pGPM);
//
// Step 2: Retreive the SOM ( Scope of Management object )
// Retrieve a SOM object associated with the target domain
//
hr = pGPM->GetDomain( (BSTR)bstrFQDN, NULL, 0, &pDom);
//
// Step 3: initialize the search string into a variant.
//
_variant_t vData( strDispName );
//
// Step 4:
// Retreive the GPMC constants object.
//
hr = pGPM->GetConstants( &pGPMConstants );
if( FAILED(hr) )
{
bErrorStr = L"Failed to retrieve IGPMConstants";
goto CLEANUP_CONTAINS2;
}
//
// Step 5:
// Retrieve the search criteria object.
//
hr = pGPM->CreateSearchCriteria( &pIGPMSearch);
if( FAILED(hr) )
{
bErrorStr = L"Failed to create IGPMSearchCirteria";
goto CLEANUP_CONTAINS2;
}
//
// Step 6 & 7: Setup the search property and the
// search operation constants.
//
GPMSearchProperty sp;
GPMSearchOperation so;
//
// Contains Match for display name.
//
pGPMConstants->get_SearchPropertyGPODisplayName( &sp);
pGPMConstants->get_SearchOpContains( &so);
//
// Step 8:
// Add the search operation and property
// to the search criteria object.
//
pIGPMSearch->Add( sp, so, vData );
long lCount = 0;
//
// Step 9:
// Perform the search and retrieve the GOP collection.
//
hr = pDom->SearchGPOs( pIGPMSearch, &pGPOCollection);
if( FAILED(hr) )
{
bErrorStr = L"IGPMDomain::SearchGPOs failed";
vData.Clear();
goto CLEANUP_CONTAINS2;
}
//
// the following is an example of how to work
// with the GPO collection returned from the search.
// If we matched at least one item, lets use a helper
// function to retrieve the index of the GPO object
// from the collection.
//
pGPOCollection->get_Count( &lCount);
printf("Found %d GPOs that matched the display name criteria \"%S\"\n",
lCount, strDispName);
if( lCount > 0 )
{
long lIndex = 0;
//
// Get a selected GPO to delete
// if the count is 1, then we have only 1 item,
// if the count is > 1, then we need to
// ask the user to select one to modify.
//
if( lCount == 1 ) lIndex = 1;
else lIndex = RequestGPOIndex( pGPOCollection );
if( lIndex > 0 )
{
//
// Retrieve the mode and settings
//
int iMode = RequestMode();
DWORD lData = RequestSetting();
//
// Clear the variant to request an IDispatch
// pointer to the IGPMGPO interface that
// represents the GPO.
//
vData.Clear();
hr = pGPOCollection->get_Item( lIndex, &vData);
if( FAILED(hr) )
{
bErrorStr = L"Failed to retrieve IGPMGPO interface form collection";
goto CLEANUP_CONTAINS2;
}
//
// QI for the GPO interface
//
hr = vData.pdispVal->QueryInterface( IID_IGPMGPO,
(void **)&pGPO);
if( FAILED(hr) )
{
bErrorStr = L"Failed to retreive GPO pointer from vData.pdispVal->QueryInterface";
vData.Clear();
goto CLEANUP_CONTAINS2;
}
//
// Get the path to the GPO and then call the
// modify function.
//
BSTR bPath;
pGPO->get_Path( &bPath);
_bstr_t bADsPath("LDAP://");
bADsPath = bADsPath + strFQDN;
bADsPath = bADsPath + "/";
bADsPath = bADsPath + bPath;
SysFreeString( bPath );
hr = ModifyUserPolicyForPreventAccessToCmdPrompt(
bADsPath, iMode, lData);
if( SUCCEEDED(hr) ) bErrorStr = L"Successfully modified GPO";
else bErrorStr = L"Unable to modify GPO";
}
CLEANUP_CONTAINS2:
vData.Clear();
if( pGPO ) pGPO->Release();
if( pGPOCollection) pGPOCollection->Release();
if ( pIGPMSearch ) pIGPMSearch->Release();
if( pGPMConstants ) pGPMConstants->Release();
if( pGPM ) pGPM->Release();
if( pDom ) pDom->Release();
LPTSTR pstr = bErrorStr;
printf("%S\nLast HRESULT: %0X\n", pstr, hr);
CoUninitialize();
return hr;
}
}
break;
Below are 3 helper functions:
RequestGPOIndex – example of working with the GPO collection returned from the search.
RequestMode – retrieves the mode setting for the GPO.
RequestSetting – Retreives the data to be written to the registry.
int RequestMode ( )
{
int retval = -1;
BOOL bContinue = TRUE;
fflush(stdin);
while( (retval < 0 ) || ( retval > 2 ) )
{
printf("\nEnter GPO Mode Value:\n 0 - Not Configured\n 1 - Enabled\n 2 - Disabled\nEnter Selection: ");
fscanf_s(stdin, "%d", &retval);
if( (retval < 0 ) || ( retval > 2 )) printf("Invalid Mode Values, must be 0, 1, 2\n");
fflush(stdin);
}
return retval;
}
DWORD RequestSetting( )
{
int retval = -1;
BOOL bContinue = TRUE;
fflush(stdin);
while( (retval < 1 ) || ( retval > 2 ) )
{
printf("\nDisable the command prompt script processing also?\n 1 - Yes\n 2 - No\nEnter Selection: ");
fscanf_s(stdin, "%d", &retval);
if( (retval < 1 ) || ( retval > 2 )) printf("Invalid Mode Values, must be 1, 2\n");
fflush(stdin);
}
return retval;
}
long RequestGPOIndex( IGPMGPOCollection *pGPOs)
{
long lSelection = -1;
long lCount = 0;
if( pGPOs )
{
pGPOs->get_Count( &lCount );
_variant_t vData;
IGPMGPO *pGPO;
while ( (lSelection < 0) || (lSelection > lCount ) )
{
long lLoop = 1;
printf("GPOs in Collection:\n");
BSTR bID;
VARIANT_BOOL bUser;
VARIANT_BOOL bMachine;
HRESULT hr;
for( lLoop =1 ;lLoop <= lCount; lLoop++ )
{
hr = pGPOs->get_Item( lLoop, &vData );
hr = vData.pdispVal->QueryInterface(IID_IGPMGPO, (void **)&pGPO);
pGPO->get_ID( &bID);
pGPO->IsComputerEnabled( &bMachine );
pGPO->IsUserEnabled( &bUser);
printf(" %d -> ID: \"%S\"\n", lLoop, bID );
printf("\tUser Enabled: ");
if( bUser == VARIANT_TRUE ) printf(" Yes ");
else printf(" No ");
printf(" Machine Enabled: ");
if( bMachine ) printf(" Yes \n");
else printf(" No\n");
SysFreeString( bID );
pGPO->get_DisplayName( &bID );
printf("\tDisplay Name: \"%S\"\n", bID);
SysFreeString( bID );
pGPO->Release();
vData.Clear();
}
printf(" 0 -> No Selection\n\nEnter a number from 0 to %d: " , lCount);
fflush(stdin);
fscanf_s(stdin,"%d",&lSelection);
if( (lSelection < 0) || (lSelection > lCount) ) printf("\n\007Invalid Entry\n");
}
if( lSelection == 0 ) lSelection = -1;
}
return lSelection;
}
|
-
In my last bog post:
http://blogs.msdn.com/dsadsi/archive/2009/07/23/working-with-group-policy-objects-programmatically-determining-registry-values-to-enable-disable-set-a-specific-policy.aspx
I discussed an empirical method to determine the values one needs to write to the registry to enable/disable/set a registry based group policy. In this post, I will provide a simple C++ function that illustrates how to write the key information into the GPO.
For now, lets concentrate on the C++ code for modifying the GPO.
The function requires 3 pieces of information:
- ADsPath to the GPO object to modify
- A mode control values, 0=Not Configured, 1=Enabled, 2=Disabled
- The actual value to write in the key that represents the mode control value.
The source for the function follows. The comments should provide insight into how the code works.
One key point is to realize that if you are modifying an existing GPO that contains multiple settings on a specific registry key tree, you cannot just delete the entire key tree, you must delete just the values that affect the settings you are working with.
Another subtle gotcha is the way you use the IGroupPolicyObject::GetRegistryKey and the IGroupPolicyObject::Save methods. The flag on in GetRegistryKey to indicate the section is an unsigned valued ( GPO_SECTION_ROOT or GPO_SECTION_USER or GPO_SECTION_MACHINE) , the flag to indicate the section on the Save method is a boolean ( TRUE to write the machine configuration, FALSE to write the user configuration). I made the mistake of using the same value in both locations, and thought I was chasing a bug in the IGroupPolicyObject interface.
Finding the path can be a challenge. The next blog post will provide details on how to locate a GPO object in the AD based on specific information using either the GPMC object model or the BrowseForGPO function.
C++ Source Code follows:
HRESULT ModifyUserPolicyForPreventAccessToCmdPrompt( BSTR bGPOPath, int iMode, DWORD lData)
{
HRESULT hr=S_OK;
//
// Use IGroupPolicyObject to retrieve and modify the registry settings.
// for the GPO represented by the gpoInfo.lpDsPath
//
IGroupPolicyObject* p = NULL;
hr = CoCreateInstance(CLSID_GroupPolicyObject, NULL,
CLSCTX_INPROC_SERVER, IID_IGroupPolicyObject,
(LPVOID*)&p);
if (SUCCEEDED(hr))
{
//
// The GPO value we want to modify is the
//
// User Configuration
// +> Policies
// +>Administrative Templates
// +->System
// +->Prevent access to the command prompt
//
DWORD dwSection = GPO_SECTION_USER;
HKEY hGPOSectionKey = NULL;
DWORD dwData;
HKEY hSettingKey;
LSTATUS rStatus;
hr = 0;
//
//Open the GPO and load its registy values for both: Machine and user
//
hr = p->OpenDSGPO( bGPOPath, GPO_OPEN_LOAD_REGISTRY);
//
// Request the user Registy hive for the GPO
//
hr = p->GetRegistryKey(dwSection, &hGPOSectionKey);
//
// Determine if you want to set it to Not Congigure,
// Enabled or Disabled for the GPO itself.
//
// The second call, RequestSetting will provide the "Yes" or "No"
// value for setting
// the policy as shown by the GPO Editor
//
// iMode
// 0=Not Configured, 1=Enabled, 2=Disabled
//
switch (iMode)
{
case 0:
//
// We do not want to configure the GPO, but we don't want to
// affect other GPOs on the same key,
// so just delete values associated with this
// particular GPO setting.
//
rStatus = RegDeleteValue(hGPOSectionKey,
L"Software\\Policies\\Microsoft\\Windows\\System\\DisableCMD"
);
rStatus = RegDeleteValue(hGPOSectionKey,
L"Software\\Policies\\Microsoft\\Windows\\System\\**del.DisableCMD"
);
break;
case 1:
{
//
// To enable the policy, the DisableCMD value must
// exist and the **del.DisableCMD value must not.
//
// lData:
//
// Check to see if the key for this policy exists.
// If if it does, retrieve a handle
// If not, create it.
//
if( RegOpenKeyEx( hGPOSectionKey,
L"Software\\Policies\\Microsoft\\Windows\\System", 0,
KEY_WRITE, &hSettingKey) != ERROR_SUCCESS )
{
rStatus = RegCreateKeyEx(
hGPOSectionKey,
L"Software\\Policies\\Microsoft\\Windows\\System",
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
NULL,
&hSettingKey,
NULL );
}
//
// Set the value DisableCMD and allow, disallow
// script launching of CMD
//
rStatus = RegSetValueEx(hSettingKey, L"DisableCMD",
NULL, REG_DWORD, (BYTE *)(&lData),
sizeof(DWORD));
//
// Remove the not configured value indicator from the hive.
// It may not exist, so the RegDeleteValue may return
// and error, this can be ignored.
//
rStatus = RegDeleteValue(hGPOSectionKey,
L"Software\\Policies\\Microsoft\\Windows\\System\\**del.DisableCMD"
);
rStatus = RegCloseKey(hSettingKey);
break;
}
case 2:
{
//
// Disable the policy.
// must remove the DisableCMD value and add the
// **del.DisableCMD value.
//
// Same stesp as before, check to see if the key for this
// policy exists,
// if not, create it.
//
BOOL bCreate = FALSE;
if( RegOpenKeyEx( hGPOSectionKey, L"Software\\Policies\\Microsoft\\Windows\\System", 0, KEY_WRITE, &hSettingKey) != ERROR_SUCCESS )
{
rStatus = RegCreateKeyEx(
hGPOSectionKey,
L"Software\\Policies\\Microsoft\\Windows\\System",
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
NULL,
&hSettingKey,
NULL );
bCreate = TRUE;
}
DWORD dwType = 0;
DWORD cbType = sizeof( dwData );
if( !bCreate )
{
//
// If we did not create the key, then our value
// *may* exist.
// try to read it. If we succeed, write that value back
// to **del.DisableCMD
// if not, then set **del.DisableCMD to 0
//
rStatus = RegGetValue(hGPOSectionKey,
L"Software\\Policies\\Microsoft\\Windows\\System", L"DisableCMD", RRF_RT_ANY, &dwType, (BYTE *)(&dwData), &cbType);
if( rStatus != ERROR_SUCCESS ) dwData = 0;
else RegDeleteValue( hSettingKey, L"DisableCMD");
rStatus = RegSetValueEx(hSettingKey, L"**del.DisableCMD", NULL, REG_DWORD, (BYTE *)(&dwData), sizeof(DWORD));
}
else
{
//
// The key was created, just set the **del.DisableCMD
// value to 0
//
dwData = 0;
rStatus = RegSetValueEx(hSettingKey, L"**del.DisableCMD", NULL, REG_DWORD, (BYTE *)(&dwData), sizeof(DWORD));
}
rStatus = RegCloseKey(hSettingKey);
}
}
GUID RegistryId = REGISTRY_EXTENSION_GUID;
GUID ThisAdminToolGuid =
/*{ CLSID_PolicySnapinUser/* */
{
0x0F6B957E,
0x509E,
0x11D1,
{0xA7, 0xCC, 0x00, 0x00, 0xF8, 0x75, 0x71, 0xE3}
};
rStatus = RegCloseKey(hGPOSectionKey);
//
// Write the GPO back to the directory
//
hr = p->Save(
FALSE,
TRUE,
&RegistryId,
&ThisAdminToolGuid );
hr = p->Release();
}
return hr;
}
|
-
Greetings!
Over the past few months our team has seen a number of customer requesting information on how to programmatically/create/edit/read registry based GPO information. I took some time to combine a couple of samples into one that illustrates a number of these concepts.
The first question one must answer when working with a registry based GPO is the programmatic modification of this GPO setting supported by Microsoft. The general answer to that question is that if their exist public documentation on the MSDN or Tech Net site that provides information on the registry key paths and the possible values then, yes, programmatic modification is supported. In my sample, I used the excel spreadsheet available at the following MSDN link:
http://www.microsoft.com/downloads/details.aspx?familyid=41dc179b-3328-4350-ade1-c0d9289f09ef&displaylang=en
The spreadsheet contains a list of policies and their associated registry keys.
Remember, the GPO can have information stored in 3 different places:
- The Active Directory in the form of a GroupPolicyContainer object.
- The Sysvol of any DC, where GPO related INI file or POL file is created. The POL file is then written into the local machine registry in either HKEY_LOCAL_MACHINE or HKEY_CURRENT_USER. In addition to these two files, the GPO could store private data in a format known only to the GPO extension.
- The client system where a GPO extension could exist to process GPO private data.
The next step is to determine exactly what values need to be written to the keys in order to enable/disable/set the policy. While the key values are generally documented, there can be subtle differences in the values that the GUI recognizes and uses to determine if a GPO is enable/disable or set. I find that using the empirical method works best to solve this problem. Using the Group Policy Management Console GUI tool, I create a GPO with a specific display name like "MaxVs Test Policy". I set the policy that I want to set programmatically. Then I do a query in the Active Directory Users and computers tool to locate the GPO Object in the Active Directory. The GPO object contains a CN that is a GUID. This GUID can be used to locate the NTFS folder on the SYSVOL of a DC that the policy registry information was written to.
For example, if I created a GPO that sets the "Prevent access to the command prompt" under the "User Configuration" node:
User Configuration -> Policies -> Administrative Templates -> Security -> "Prevent access to the command prompt"
I enable it and set it. When I click the Apply button, a Registry.POL file will be created on the sysvol in a directory named after the common name of the GroupPolicyContainer object created in the AD. The path will look similar to the following:
C:\Windows\SYSVOL\sysvol\maxv08.nttest.microsoft.com\Policies\{529AB3E5-3818-42F5-9BDE-95629F33780C}\User
Where "maxv08.nttest.microsoft.com" will be the domain name of your domain and the GUID string will be the display name of the GPO you just created. At this point, you can view the POL file in notepad or using a binary file editor. If you use notepad, the contents would be similar to the following:
PReg [ S o f t w a r e \ P o l i c i e s \ M i c r o s o f t \ W i n d o w s \ S y s t e m ; D i s a b l e C M D ; ; ; ]
To see the actual values written in the key, you would need to use a binary editor that displays the binary representation for the values displayed above.
Copy this file to another location with a name that indicates the setting values.
Repeat this process for all the settings you wish to duplicate programmatically.
Once you know the registry values to write, we are ready to write the C++ code to set them.
Keep watching this blog, I will make another post that provides a function that illustrates how to set the "Prevent access to the command prompt" policy.
|
-
Something interesting that I discovered working with an LDIFDE dump. I need to verify that a base64 encoded string was associated with a specific user SID. In the past, I was forced to hack this information out in using a couple of ldap helper functions in C++. The code necessary to to accomplish this task was approximately 20 to 30 lines.
Being the inquisitive person that I am, I began to wonder if the newer versions of the .Net framework ( 2.0 an higher) had any intrinsic functions that would do the same sort of work. Low and behold, I found exactly what I was looking for. The Convert name space contains a number of very useful conversion method, one of which is Convert.FromBase64String that returns an byte array.
Using this method, I can quickly convert any base64 encoded data from my LDIFDE dumps into their binary equivalents, then I can use the .Net object type that matches my particular binary data to convert the binary blob to a string.
The following C# code illustrates how you can convert a base64 encoded guid and a base64 encoded SID into their more human readable string forms:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace decodebase64SID
{
class Program
{
static void Main(string[] args)
{
string strSID = "AQUAAAAAAAUVAAAAPXj3WXwxNhGBjE3aTQQAAA==";
byte[] sid = Convert.FromBase64String(strSID);
System.Security.Principal.SecurityIdentifier SI = new System.Security.Principal.SecurityIdentifier(sid, 0);
Console.WriteLine(SI.ToString());
string strGUID = "shgJ1o5gbEurVpGXHpyhCg==";
byte[] bGuid = Convert.FromBase64String(strGUID);
System.Guid gu = new System.Guid(bGuid);
Console.WriteLine(gu.ToString());
}
}
}
|
-
Problem:
When running a script to add a user from forest Fabrikam, to a domain local group in forest Microsoft, you will receive an error as follows:
80072030 "There is no such object on the server"
This is a snippet of the code we tried to perform the procedure:
================================================= set objGroup = GetObject("LDAP:// microsoft.com /cn=Domain Local Group,ou=Test,dc=microsoft,dc=com") set objUser = GetObject("LDAP://fabrikam.com/cn=Vishal Joshi,ou=CrossDomainTestOU,dc=fabrikam,dc=com")
wscript.echo "Group:" & objgroup.adspath & vbcrlf & "User:" & objUser.adspath
objGroup.add(objUser.adspath) ===================================================
If the domains are in two forests with a two way trust and running at 2003 native forest functional level, it will not work.
You will have no trouble using Active Directory Users & Computers to add the user to the group.
The echo works in all the above scenarios, so you know that the GetObject is working. The script will always fail on the objGroup.add.
Even adding the serverbind to the LDAP requests the script will not work.
Also running under the user who has enough right’s on both domains.
Resolution:
We need to use the LDAP://<SID=S-1-5-21-2052111302-2139871995-839522115-1115> format while adding the user
Something like
objGroup.add(“LDAP://<SID=S-1-5-21-2052111302-2139871995-839522115-1115>”)
What happens when you use the SID bind string, it creates a place holder.
If you provide the path, it tries to verify the object and retrieve the SID to create the FSP, because the FSP is what is added to the local domain group.
It did not check to see if the SID was valid... it just accepted it because it was a valid SID. Meaning, it was the correct size with the correct number of sub authorities.
So it’s the callers responsibility to send the right SID to this call.
Since the user is in a foreign forest, the object will not represented in local GC. So the foreign user should be added to the local domain group using the SID of the user object instead of distinguished name.
|
-
If you build a 32 bit executable version of the PropSheetHost SDK sample and then try to execute it on an x64 Vista machine without the x64 Microsoft Remote Server Administration Tools for Windows (RSAT) Vista installed, you will see just two property sheet tabs. Once you have installed and enable the RSAT tool, as soon as you start the program and enter a valid ADSPath, you will get dialog box "The Resource loader cache doesn't have the loaded MUI entry".
This is caused by a missing MUI file required by the WoW64 layer to display the resources exposed by the RSAT tools. Even with the RSAT tools, you will still get the resource error because the 32 bit MUI files required by the WoW64 sub system are not included in the 64 bit RSAT install.
The work around for this problem is straight forward.
1. Download the x86 version of the RSAT install package from the MS download site:
http://www.microsoft.com/downloads/details.aspx?FamilyID=9ff6e897-23ce-4a36-b7fc-d52065de9960&DisplayLang=en
2. Create a local directory and save the file to your x64 Vista machine.
3. Use the Expand command to extract the CAB file from the MSU package following the instructions documented in the KB:
http://support.microsoft.com/kb/928636
4. Look for the CAB cabinet file in your expansion directory. You should see two, one with the KB article in the title one that starts with WSUSSCAN.CAB. Use the cab file with the KB number in it.
The mui file required to make the sample work is ADPROP.DLL.MUI, so you now use the expand command to extract just that MUI file from the CAB file. The command line will look something like:
expand -F:adprop.dll.mui <Path_to_CAB_File_Extracted_From_MSU_File> <Path_To_Directory_To_Expand_MUI_File>
After executing this command, locate the adprop.dll.mui file. Copy this file to the <Windows Install Directory>\SysWoW64\en-US
Now the PropSheetHost SDK sample should work just as it did on x86 windows.
|
-
Password Settings objects were introduced in Windows 2008 AD to allow administrators more control over the password policies for the domain. These objects can be associated with a container of user objects or groups.
The following link provides details on how PSOs are applied:
http://msdn.microsoft.com/en-us/library/cc212097(PROT.10).aspx
When you create a PSO, there are 10 properties that must be set:
msds-maximumpasswordage - a 64 bit time value interval represented in nanoseconds before the password expires. msds-minimumpasswordage - a 64 bit time value interval represented in nanoseconds before a password change is allowed. msds-LockoutObservationWindow -a 64 bit time value interval in nanoseconds to look for possible bad password entries msds-LockoutDuration - a 64 bit time value interval in nanoseconds to lock the account after a specified number of bad password attempts during the observation period. msds-LockoutThreshold - a 16-bit unsigned integer indicating the number of bad password attempts within an Effective-LockoutObservationWindow that will cause an account to be locked out. msds-PasswordSettingsPrecedence - an integer value used to resolve conflicts with multiple PSOs. The PSO with the lowest precedence value is applied. msds-PasswordHistoryLength- number of old passwords that are retained to prevent duplication of a previous password. msds-MinimumPasswordLength - minim length of the password. msDS-PasswordComplexityEnabled - Boolean value, if true, password complexity rules apply to new passwords. msDS-PasswordReversibleEncryptionEnabled - A Boolean value indicating that the user's clear text password is to be stored in the supplemental Credentials attribute.
Most of these properties are straight forward types and can be easily converted from native Powershell types. The time intervals are represented as 64 bit negative values for the number of nanoseconds each property should be in effect. The following KB provides some useful information on how to create these values using long value types: http://support.microsoft.com/kb/954414
There is a native .Net type that works with nanosecond intervals called System.TimeSpan. If you use the static Parse method, you can provide a string representation for the time interval using the following format:
"Days.hours:Minutes:Seconds:Milliseconds"
So, if you wanted to create a TimeSpan object for time interval representing 2 days, 3 hours, 15 minutes, 30 seconds, the string would look like: "3.03:15:30". To create a TimeSpan object in PSH using this value, the line of code would be:
$timeInterval = [System.TimeSpan]::Parse("3.03:15:30")
Using the System.TimeSpan.Tick property, you can retrieve the total nanoseconds. Just multiply by -1 and the value is in the format that needs to be stored to the Active Directory.
We still have one problem to solve, we need to take this 64 bit integer and convert it into a format that ADSI can send to the AD. We need to convert this value into an IADsLargeInteger interface. I know my solution for this conversion is not the most elegant one, however, I was having trouble trying to figure out how I could remove the top 32 bits of the 64 bit value to load them into the IADsLargeInteger::HighPart.
I used Adam Weigert's Blog post as a basis, then created my own home grown System.TimeSpan to IADsLargeInteger conversion routine. Many thanks to Adam for posting the conversion information. Below is a link to his blog post:
http://weblogs.asp.net/adweigert/archive/2007/03/23/powershell-convert-active-directory-iadslargeinteger-to-system-int64.aspx
Once I was able to convert the ticks to IADsLargeInteger types, the creation of the PSO was a piece of cake.
Using the TimeSpan object makes it very easy to create human readable time spans. No mucking about with large conversion multipliers, we let the TimeSpan::Parse method do all the work.
Below is the PSH code to create a PSO in a 2008 Functional Mode domain:
# # Conversion function to take TimeSpan objects # and create IADsLargeInteger objects # function ConvertTimeSpanToTimeIntervalIADsLargeInteger([System.TimeSpan] $ts, [object]$olG) { [Int64] $i64 = [Int64]$ts.Ticks * -1; $i64 $bytes = [System.BitConverter]::GetBytes($i64) # # Ok, I know this is not very elegant or Powershellish.... # but IT WORKS! # I would love to see a better way of working with bits in PSH # $tmpHigh = [System.Byte[]]@(0,0,0,0); $tmpLow = [System.Byte[]]@(0,0,0,0); [System.Array]::Copy($bytes,0,$tmpLow,0,4); [System.Array]::Copy($bytes,4,$tmpHigh,0,4); $lowPart = [System.BitConverter]::ToInt32($tmpLow, 0) $highPart = [System.BitConverter]::ToInt32($tmpHigh, 0) $oLG.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::SetProperty, $null, $oLG, @($highPart)) $oLG.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::SetProperty, $null, $olG, @($lowPart)) # return $oLG } # # Function to convert IADsLargeIntegers to 64 bit values # Adam Weigert's conversion routine # function ConvertADSLargeInteger([object] $adsLargeInteger) { $highPart = $adsLargeInteger.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null) $lowPart = $adsLargeInteger.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)
$bytes = [System.BitConverter]::GetBytes($highPart) $tmp = [System.Byte[]]@(0,0,0,0,0,0,0,0) [System.Array]::Copy($bytes, 0, $tmp, 4, 4) $highPart = [System.BitConverter]::ToInt64($tmp, 0)
$bytes = [System.BitConverter]::GetBytes($lowPart) $lowPart = [System.BitConverter]::ToUInt32($bytes, 0) return $lowPart + $highPart } # # Replace $Server with your target server # Replace $UserID with your credentials # Replace $Password with the appropriate password # $Server = "my.Server.My.Domain.Com" $UserID = "Domain\UserID" $Password = "Password" # # Build a ROOT DSE path so you can retrieve the # DefaultNamingContext # $rootpath = "LDAP://" + $Server + "/RootDSE" $oRoot = new-object System.DirectoryServices.DirectoryEntry($rootpath, $UserID, $Password) $oRoot.PSBase.RefreshCache() $strDNC = $oRoot.defaultnamingcontext # # Build the path to the default password settings container # $strContainerPath = "LDAP://" + $Server + "/CN=Password Settings Container,CN=System," + $strDNC $strContainerPath $oPWDContainer = new-object System.DirectoryServices.DirectoryEntry($strContainerPath, $UserID, $Password); $oPWDContainer.PSBase.RefreshCache() # # Create a PasswordSetting Object, then fill it with values # $oObj = $oPWDContainer.PSBase.Children.Add("CN=My PSO Object41", "msds-passwordsettings"); # # Setup the individual ticks objects # Using KB Defaults described in KB954414: # http://support.microsoft.com/kb/954414 # $oLarge2Days = new-object -ComObject "LargeInteger" $oLarge1Day = new-object -ComObject "LargeInteger" $oLarge30Minutes = new-Object -ComObject "LargeInteger" # # setup the time spans # $TM2days = [System.TimeSpan]::Parse("2.00:00:00"); $TM30Minutes = [System.TimeSpan]::Parse("00:30:00"); $TM1Day = [System.TimeSpan]::Parse("1.00:00:00"); # # Convert the TimeSpans to Time Interval LargeIntegers # ConvertTimeSpanToTimeIntervalIADsLargeInteger $TM2Days $oLarge2Days ConvertTimeSpanToTimeIntervalIADsLargeInteger $TM1Day $oLarge1Day ConvertTimeSpanToTimeIntervalIADsLargeInteger $TM30Minutes $oLarge30Minutes # # Set the precedence, Reverse Encryption, PasswordLengths and other values # $PolicyPrecedence = 20 $ReversibleEncryption = $false $ComplexityRequirements = $true $LockoutThreshold = 0 $MinPasswordLength = 8 $HistoryLength = 20 # # Load up the Properties # $oObj.PSBase.Properties["msds-maximumpasswordage"].Value = $oLarge2Days $oObj.PSBase.Properties["msds-minimumpasswordage"].Value = $oLarge1Day $oObj.PSBase.Properties["msds-LockoutObservationWindow"].Value = $oLarge30Minutes $oObj.PSBase.Properties["msds-LockoutDuration"].Value = $oLarge30Minutes $oObj.PSBase.Properties["msds-LockoutThreshold"].Value = $LockoutThreshold $oObj.PSBase.Properties["msds-PasswordSettingsPrecedence"].Value = $PolicyPrecedence $oObj.PSBase.Properties["msds-PasswordHistoryLength"].Value = $HistoryLength $oObj.PSBase.Properties["msds-MinimumPasswordLength"].Value = $MinPasswordLength $oObj.PSBase.Properties["msDS-PasswordComplexityEnabled"].Value = $ComplexityRequirements $oObj.PSBase.Properties["msDS-PasswordReversibleEncryptionEnabled"].Value = $ReversibleEncryption $oObj.PSBase.CommitChanges()
|
|
|
|