|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Globalization;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using MACLPrinc = System.Security.Principal;
namespace System.DirectoryServices.AccountManagement
{
internal partial class ADStoreCtx : StoreCtx
{
protected DirectoryEntry ctxBase;
private const int mappingIndex = 0;
private readonly object _ctxBaseLock = new object(); // when mutating ctxBase
private readonly bool _ownCtxBase; // if true, we "own" ctxBase and must Dispose of it when we're done
private bool _disposed;
protected internal NetCred Credentials { get { return this.credentials; } }
protected NetCred credentials;
protected internal AuthenticationTypes AuthTypes { get { return this.authTypes; } }
protected AuthenticationTypes authTypes;
protected ContextOptions contextOptions;
protected internal virtual void InitializeNewDirectoryOptions(DirectoryEntry newDeChild)
{
}
//
// Static constructor: used for initializing static tables
//
static ADStoreCtx()
{
//
// Load the filterPropertiesTable
//
LoadFilterMappingTable(mappingIndex, s_filterPropertiesTableRaw);
LoadPropertyMappingTable(mappingIndex, s_propertyMappingTableRaw);
}
protected virtual int MappingTableIndex
{
get
{
return mappingIndex;
}
}
protected static void LoadFilterMappingTable(int mappingIndex, object[,] rawFilterPropertiesTable)
{
if (null == s_filterPropertiesTable)
s_filterPropertiesTable = new Hashtable();
Hashtable mappingTable = new Hashtable();
for (int i = 0; i < rawFilterPropertiesTable.GetLength(0); i++)
{
Type qbeType = rawFilterPropertiesTable[i, 0] as Type;
string adPropertyName = rawFilterPropertiesTable[i, 1] as string;
FilterConverterDelegate f = rawFilterPropertiesTable[i, 2] as FilterConverterDelegate;
Debug.Assert(qbeType != null);
Debug.Assert(f != null);
// There should only be one entry per QBE type
Debug.Assert(mappingTable[qbeType] == null);
FilterPropertyTableEntry entry = new FilterPropertyTableEntry();
entry.suggestedADPropertyName = adPropertyName;
entry.converter = f;
mappingTable[qbeType] = entry;
}
s_filterPropertiesTable.Add(mappingIndex, mappingTable);
}
protected static void LoadPropertyMappingTable(int mappingIndex, object[,] rawPropertyMappingTable)
{
//
// Load the propertyMappingTableByProperty and propertyMappingTableByLDAP tables
//
if (null == s_propertyMappingTableByProperty)
s_propertyMappingTableByProperty = new Hashtable();
if (null == s_propertyMappingTableByLDAP)
s_propertyMappingTableByLDAP = new Hashtable();
if (null == s_propertyMappingTableByPropertyFull)
s_propertyMappingTableByPropertyFull = new Hashtable();
if (null == TypeToLdapPropListMap)
TypeToLdapPropListMap = new Dictionary<int, Dictionary<Type, StringCollection>>();
Hashtable mappingTableByProperty = new Hashtable();
Hashtable mappingTableByLDAP = new Hashtable();
Hashtable mappingTableByPropertyFull = new Hashtable();
Dictionary<string, string[]> propertyNameToLdapAttr = new Dictionary<string, string[]>();
Dictionary<Type, StringCollection> TypeToLdapDict = new Dictionary<Type, StringCollection>();
for (int i = 0; i < s_propertyMappingTableRaw.GetLength(0); i++)
{
string propertyName = rawPropertyMappingTable[i, 0] as string;
string ldapAttribute = rawPropertyMappingTable[i, 1] as string;
FromLdapConverterDelegate fromLdap = rawPropertyMappingTable[i, 2] as FromLdapConverterDelegate;
ToLdapConverterDelegate toLdap = rawPropertyMappingTable[i, 3] as ToLdapConverterDelegate;
Debug.Assert(propertyName != null);
Debug.Assert((ldapAttribute != null && fromLdap != null) || (fromLdap == null));
//Debug.Assert(toLdap != null);
// Build the table entry. The same entry will be used in both tables.
// Once constructed, the table entries are treated as read-only, so there's
// no danger in sharing the entries between tables.
PropertyMappingTableEntry propertyEntry = new PropertyMappingTableEntry();
propertyEntry.propertyName = propertyName;
propertyEntry.suggestedADPropertyName = ldapAttribute;
propertyEntry.ldapToPapiConverter = fromLdap;
propertyEntry.papiToLdapConverter = toLdap;
// Build a mapping table from PAPI propertyname to ldapAttribute that we can use below
// to build a list of ldap attributes for each object type.
if (null != ldapAttribute)
{
if (propertyNameToLdapAttr.TryGetValue(propertyName, out string[] ldapAttributes))
{
string[] props = new string[ldapAttributes.Length + 1];
ldapAttributes.CopyTo(props, 0);
props[ldapAttributes.Length] = ldapAttribute;
propertyNameToLdapAttr[propertyName] = props;
}
else
propertyNameToLdapAttr.Add(propertyName, new string[] { ldapAttribute });
}
// propertyMappingTableByProperty
// If toLdap is null, there's no PAPI->LDAP mapping for this property
// (it's probably read-only, e.g., "lastLogon").
if (toLdap != null)
{
mappingTableByProperty[propertyName] ??= new ArrayList();
((ArrayList)mappingTableByProperty[propertyName]).Add(propertyEntry);
}
mappingTableByPropertyFull[propertyName] ??= new ArrayList();
((ArrayList)mappingTableByPropertyFull[propertyName]).Add(propertyEntry);
// mappingTableByLDAP
// If fromLdap is null, there's no direct LDAP->PAPI mapping for this property.
// It's probably a property that requires custom handling, such as IdentityClaim.
if (fromLdap != null)
{
string ldapAttributeLower = ldapAttribute.ToLowerInvariant();
mappingTableByLDAP[ldapAttributeLower] ??= new ArrayList();
((ArrayList)mappingTableByLDAP[ldapAttributeLower]).Add(propertyEntry);
}
}
s_propertyMappingTableByProperty.Add(mappingIndex, mappingTableByProperty);
s_propertyMappingTableByLDAP.Add(mappingIndex, mappingTableByLDAP);
s_propertyMappingTableByPropertyFull.Add(mappingIndex, mappingTableByPropertyFull);
// Build a table of Type mapped to a collection of all ldap attributes for that type.
// This table will be used to load the objects when searching.
StringCollection principalPropList = new StringCollection();
StringCollection authPrincipalPropList = new StringCollection();
StringCollection userPrincipalPropList = new StringCollection();
StringCollection computerPrincipalPropList = new StringCollection();
StringCollection groupPrincipalPropList = new StringCollection();
foreach (string prop in principalProperties)
{
string[] attr;
if (propertyNameToLdapAttr.TryGetValue(prop, out attr))
{
foreach (string plist in attr)
{
principalPropList.Add(plist);
authPrincipalPropList.Add(plist);
userPrincipalPropList.Add(plist);
computerPrincipalPropList.Add(plist);
groupPrincipalPropList.Add(plist);
}
}
}
foreach (string prop in authenticablePrincipalProperties)
{
string[] attr;
if (propertyNameToLdapAttr.TryGetValue(prop, out attr))
{
foreach (string plist in attr)
{
authPrincipalPropList.Add(plist);
userPrincipalPropList.Add(plist);
computerPrincipalPropList.Add(plist);
}
}
}
foreach (string prop in groupProperties)
{
string[] attr;
if (propertyNameToLdapAttr.TryGetValue(prop, out attr))
{
foreach (string plist in attr)
{
groupPrincipalPropList.Add(plist);
}
}
}
foreach (string prop in userProperties)
{
string[] attr;
if (propertyNameToLdapAttr.TryGetValue(prop, out attr))
{
foreach (string plist in attr)
{
userPrincipalPropList.Add(plist);
}
}
}
foreach (string prop in computerProperties)
{
string[] attr;
if (propertyNameToLdapAttr.TryGetValue(prop, out attr))
{
foreach (string plist in attr)
{
computerPrincipalPropList.Add(plist);
}
}
}
principalPropList.Add("objectClass");
authPrincipalPropList.Add("objectClass");
userPrincipalPropList.Add("objectClass");
computerPrincipalPropList.Add("objectClass");
groupPrincipalPropList.Add("objectClass");
TypeToLdapDict.Add(typeof(Principal), principalPropList);
TypeToLdapDict.Add(typeof(GroupPrincipal), groupPrincipalPropList);
TypeToLdapDict.Add(typeof(AuthenticablePrincipal), authPrincipalPropList);
TypeToLdapDict.Add(typeof(UserPrincipal), userPrincipalPropList);
TypeToLdapDict.Add(typeof(ComputerPrincipal), computerPrincipalPropList);
TypeToLdapPropListMap.Add(mappingIndex, TypeToLdapDict);
}
//
// Constructor
//
// Throws ArgumentException if base is not a container class (as indicated by an empty possibleInferiors
// attribute in the corresponding schema class definition)
public ADStoreCtx(DirectoryEntry ctxBase, bool ownCtxBase, string username, string password, ContextOptions options)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Constructing ADStoreCtx for {0}", ctxBase.Path);
Debug.Assert(ctxBase != null);
// This will also detect if the server is down or nonexistent
if (!IsContainer(ctxBase))
throw new InvalidOperationException(SR.ADStoreCtxMustBeContainer);
this.ctxBase = ctxBase;
_ownCtxBase = ownCtxBase;
if (username != null && password != null)
this.credentials = new NetCred(username, password);
this.contextOptions = options;
this.authTypes = SDSUtils.MapOptionsToAuthTypes(options);
}
protected bool IsContainer(DirectoryEntry de)
{
//NOTE: Invoking de.SchemaEntry creates a new DirectoryEntry object, which is not disposed by de.
using (DirectoryEntry schemaDE = de.SchemaEntry)
{
if (schemaDE.Properties["possibleInferiors"].Count == 0)
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "IsContainer: not a container ({0})", schemaDE.Path);
return false;
}
return true;
}
}
//
// IDisposable implementation
//
public override void Dispose()
{
try
{
if (!_disposed)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Dispose: disposing, ownCtxBase={0}", _ownCtxBase);
if (_ownCtxBase)
ctxBase.Dispose();
_disposed = true;
}
}
finally
{
base.Dispose();
}
}
//
// StoreCtx information
//
// Retrieves the Path (ADsPath) of the object used as the base of the StoreCtx
internal override string BasePath
{
get
{
Debug.Assert(this.ctxBase != null);
return this.ctxBase.Path;
}
}
//
// CRUD
//
// Used to perform the specified operation on the Principal.
//
// Insert() and Update() must check to make sure no properties not supported by this StoreCtx
// have been set, prior to persisting the Principal.
internal override void Insert(Principal p)
{
try
{
Debug.Assert(p.unpersisted);
Debug.Assert(!p.fakePrincipal);
// Insert the principal into the store
SDSUtils.InsertPrincipal(
p,
this,
new SDSUtils.GroupMembershipUpdater(UpdateGroupMembership),
this.credentials,
this.authTypes,
true
);
// Load in all the initial values from the store
//((DirectoryEntry)p.UnderlyingObject).RefreshCache();
LoadDirectoryEntryAttributes((DirectoryEntry)p.UnderlyingObject);
// If they set p.Enabled, enable the principal
EnablePrincipalIfNecessary(p);
// If they set CannotChangePassword then we need to set it here after the object is already created.
SetPasswordSecurityifNecessary(p);
// Load in the StoreKey
Debug.Assert(p.Key == null); // since it was previously unpersisted
Debug.Assert(p.UnderlyingObject != null); // since we just persisted it
Debug.Assert(p.UnderlyingObject is DirectoryEntry);
ADStoreKey key = new ADStoreKey(((DirectoryEntry)p.UnderlyingObject).Guid);
p.Key = key;
// Reset the change tracking
p.ResetAllChangeStatus();
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Insert: new GUID is ", ((DirectoryEntry)p.UnderlyingObject).Guid);
}
catch (PrincipalExistsException)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Insert, object already exists");
throw;
}
catch (System.SystemException e)
{
try
{
GlobalDebug.WriteLineIf(GlobalDebug.Error, "ADStoreCtx", "Insert, Save Failed (attempting to delete) Exception {0} ", e.Message);
if (null != p.UnderlyingObject)
{
SDSUtils.DeleteDirectoryEntry((DirectoryEntry)p.UnderlyingObject);
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Insert, object deleted");
}
else
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Insert, No object was created nothing to delete");
}
}
catch (System.Runtime.InteropServices.COMException deleteFail)
{
// The delete failed. Just continue we will throw the original exception below.
GlobalDebug.WriteLineIf(GlobalDebug.Error, "ADStoreCtx", "Insert, Deletion Failed {0} ", deleteFail.Message);
}
if (e is System.Runtime.InteropServices.COMException)
throw ExceptionHelper.GetExceptionFromCOMException((System.Runtime.InteropServices.COMException)e);
else
throw;
}
}
internal override bool AccessCheck(Principal p, PrincipalAccessMask targetPermission)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "AccessCheck " + targetPermission.ToString());
switch (targetPermission)
{
case PrincipalAccessMask.ChangePassword:
return CannotChangePwdFromLdapConverter((DirectoryEntry)p.GetUnderlyingObject());
default:
GlobalDebug.WriteLineIf(GlobalDebug.Error, "ADStoreCtx", "Invalid targetPermission in AccessCheck");
break;
}
return false;
}
/// <summary>
/// If The enabled property was set on the principal then perform actions
/// necessary on the principal to set the enabled status to match
/// the set value.
/// </summary>
/// <param name="p"></param>
private void EnablePrincipalIfNecessary(Principal p)
{
if (p.GetChangeStatusForProperty(PropertyNames.AuthenticablePrincipalEnabled))
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "EnablePrincipalIfNecessary: enabling principal");
Debug.Assert(p is AuthenticablePrincipal);
bool enable = (bool)p.GetValueForProperty(PropertyNames.AuthenticablePrincipalEnabled);
SetAuthPrincipalEnableStatus((AuthenticablePrincipal)p, enable);
}
}
private void SetPasswordSecurityifNecessary(Principal p)
{
if (p.GetChangeStatusForProperty(PropertyNames.PwdInfoCannotChangePassword))
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "EnablePrincipalIfNecessary: enabling principal");
Debug.Assert(p is AuthenticablePrincipal);
SetCannotChangePasswordStatus((AuthenticablePrincipal)p, (bool)p.GetValueForProperty(PropertyNames.PwdInfoCannotChangePassword), true);
}
}
private static void SetCannotChangePasswordStatus(Principal ap, bool userCannotChangePassword, bool commitChanges)
{
Debug.Assert(ap is AuthenticablePrincipal);
Debug.Assert(ap.GetUnderlyingObject() is DirectoryEntry);
DirectoryEntry de = (DirectoryEntry)ap.GetUnderlyingObject();
// retrieving ObjectSecurity after
// previously modifying the ACL will return null unless we force a cache refresh. We have to do this always,
// even before we call ObjectSecurity to see if it would return null, because once ObjectSecurity returns null the
// first time, it'll keep returning null even if we refresh the cache.
if (!de.Properties.Contains("nTSecurityDescriptor"))
de.RefreshCache(s_nTSecurityDescriptor);
ActiveDirectorySecurity adsSecurity = de.ObjectSecurity;
bool denySelfFound;
bool denyWorldFound;
bool allowSelfFound;
bool allowWorldFound;
// Scan the existing ACL to determine its current state
ScanACLForChangePasswordRight(adsSecurity, out denySelfFound, out denyWorldFound, out allowSelfFound, out allowWorldFound);
// Build the ACEs that we'll use
ActiveDirectoryAccessRule denySelfACE = new ExtendedRightAccessRule(
new MACLPrinc.SecurityIdentifier(SelfSddl),
AccessControlType.Deny,
s_changePasswordGuid);
ActiveDirectoryAccessRule denyWorldAce = new ExtendedRightAccessRule(
new MACLPrinc.SecurityIdentifier(WorldSddl),
AccessControlType.Deny,
s_changePasswordGuid);
ActiveDirectoryAccessRule allowSelfACE = new ExtendedRightAccessRule(
new MACLPrinc.SecurityIdentifier(SelfSddl),
AccessControlType.Allow,
s_changePasswordGuid);
ActiveDirectoryAccessRule allowWorldAce = new ExtendedRightAccessRule(
new MACLPrinc.SecurityIdentifier(WorldSddl),
AccessControlType.Allow,
s_changePasswordGuid);
// Based on the current state of the ACL and the userCannotChangePassword status, perform the necessary modifications,
// if any
if (userCannotChangePassword)
{
// If we want to make it so the user cannot change their password, we need to remove the ALLOW ACEs
// (if they exist) and add the necessary explicit DENY ACEs if they don't already exist.
if (!denySelfFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: add deny self");
adsSecurity.AddAccessRule(denySelfACE);
}
if (!denyWorldFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: add deny world");
adsSecurity.AddAccessRule(denyWorldAce);
}
if (allowSelfFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: remove allow self");
adsSecurity.RemoveAccessRuleSpecific(allowSelfACE);
}
if (allowWorldFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: remove allow world");
adsSecurity.RemoveAccessRuleSpecific(allowWorldAce);
}
}
else
{
// If we want to make to give the user back the right to change their password, we need to remove
// the explicit DENY ACEs if they exist. We'll also add in explicit ALLOW ACEs.
if (denySelfFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: remove deny self");
adsSecurity.RemoveAccessRuleSpecific(denySelfACE);
}
if (denyWorldFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: remove deny world");
adsSecurity.RemoveAccessRuleSpecific(denyWorldAce);
}
if (!allowSelfFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: add allow self");
adsSecurity.AddAccessRule(allowSelfACE);
}
if (!allowWorldFound)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CannotChangePwdToLdapConverter: add allow world");
adsSecurity.AddAccessRule(allowWorldAce);
}
}
if (commitChanges)
de.CommitChanges();
}
/// <summary>
/// Read the Account Control From the Directory entry. If the control is read then set or
/// clear bit 0x2 corresponding to the enable parameter
/// </summary>
/// <param name="ap">Principal to modify</param>
/// <param name="enable">New state of the enable bit</param>
///
protected virtual void SetAuthPrincipalEnableStatus(AuthenticablePrincipal ap, bool enable)
{
try
{
Debug.Assert(!ap.fakePrincipal);
int uacValue;
DirectoryEntry de = (DirectoryEntry)ap.UnderlyingObject;
if (de.Properties["userAccountControl"].Count > 0)
{
Debug.Assert(de.Properties["userAccountControl"].Count == 1);
uacValue = (int)de.Properties["userAccountControl"][0];
}
else
{
// Since we loaded the properties, we should have it. Perhaps we don't have access
// to it. In that case, we don't want to blindly overwrite whatever other bits might be there.
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "SetAuthPrincipalEnableStatus: can't read userAccountControl");
throw new PrincipalOperationException(
SR.ADStoreCtxUnableToReadExistingAccountControlFlagsToEnable);
}
if (enable && ((uacValue & 0x2) != 0))
{
// It's currently disabled, and we need to enable it
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "SetAuthPrincipalEnableStatus: Enabling (old uac={0})", uacValue);
Utils.ClearBit(ref uacValue, 0x2); // UF_ACCOUNTDISABLE
WriteAttribute(ap, "userAccountControl", uacValue);
}
else if (!enable && ((uacValue & 0x2) == 0))
{
// It's current enabled, and we need to disable it
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "SetAuthPrincipalEnableStatus: Disabling (old uac={0})", uacValue);
Utils.SetBit(ref uacValue, 0x2); // UF_ACCOUNTDISABLE
WriteAttribute(ap, "userAccountControl", uacValue);
}
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
/// <summary>
/// Apply all changed properties on the principal to the Directory Entry.
/// Reset the changed status on all the properties
/// </summary>
/// <param name="p">Principal to update</param>
internal override void Update(Principal p)
{
try
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Update");
Debug.Assert(!p.fakePrincipal);
Debug.Assert(!p.unpersisted);
Debug.Assert(p.UnderlyingObject != null);
Debug.Assert(p.UnderlyingObject is DirectoryEntry);
// Commit the properties
SDSUtils.ApplyChangesToDirectory(
p,
this,
new SDSUtils.GroupMembershipUpdater(UpdateGroupMembership),
this.credentials,
this.authTypes
);
// Reset the change tracking
p.ResetAllChangeStatus();
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
/// <summary>
/// Delete the directory entry that corresponds to the principal
/// </summary>
/// <param name="p">Principal to delete</param>
internal override void Delete(Principal p)
{
try
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Delete");
Debug.Assert(!p.fakePrincipal);
// Principal.Delete() shouldn't be calling us on an unpersisted Principal.
Debug.Assert(!p.unpersisted);
Debug.Assert(p.UnderlyingObject != null);
Debug.Assert(p.UnderlyingObject is DirectoryEntry);
SDSUtils.DeleteDirectoryEntry((DirectoryEntry)p.UnderlyingObject);
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
internal override void Move(StoreCtx originalStore, Principal p)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Move");
Debug.Assert(p != null);
Debug.Assert(originalStore != null);
Debug.Assert(originalStore is ADStoreCtx);
string name = null;
string rdnPrefix = p.ExtensionHelper.RdnPrefix;
string baseObjectRdnPrefix = null;
Type principalType = p.GetType();
if (null == rdnPrefix)
{
throw new InvalidOperationException(SR.ExtensionInvalidClassAttributes);
}
if (p.GetChangeStatusForProperty(PropertyNames.PrincipalName))
{
name = rdnPrefix + "=" + (string)p.GetValueForProperty(PropertyNames.PrincipalName);
// If the principal class is derived from Group, Computer or User then we need to see
// if the class has an RdnPrefix set that differs from the base class prefix. If so then we need
// to modify that attribute when if changed the name during the move.
if (principalType.IsSubclassOf(typeof(GroupPrincipal)) ||
principalType.IsSubclassOf(typeof(UserPrincipal)) ||
principalType.IsSubclassOf(typeof(ComputerPrincipal)))
{
DirectoryRdnPrefixAttribute[] MyAttribute =
(DirectoryRdnPrefixAttribute[])Attribute.GetCustomAttributes(principalType.BaseType, typeof(DirectoryRdnPrefixAttribute), false);
if (MyAttribute == null)
throw new InvalidOperationException(SR.ExtensionInvalidClassAttributes);
string defaultRdn = null;
// Search for the rdn prefix. This will use either the prefix that has a context type
// that matches the principals context or the first rdnPrefix that has a null context type
for (int i = 0; i < MyAttribute.Length; i++)
{
if ((MyAttribute[i].Context == null && null == defaultRdn) ||
(p.ContextType == MyAttribute[i].Context))
{
defaultRdn = MyAttribute[i].RdnPrefix;
}
}
// If the base objects RDN prefix is not the same as the derived class then we need to set both
if (defaultRdn != rdnPrefix)
{
baseObjectRdnPrefix = defaultRdn;
}
}
}
SDSUtils.MoveDirectoryEntry((DirectoryEntry)p.GetUnderlyingObject(),
ctxBase,
name);
p.LoadValueIntoProperty(PropertyNames.PrincipalName, p.GetValueForProperty(PropertyNames.PrincipalName));
if (null != baseObjectRdnPrefix)
{
((DirectoryEntry)p.GetUnderlyingObject()).Properties[baseObjectRdnPrefix].Value = (string)p.GetValueForProperty(PropertyNames.PrincipalName);
}
}
//
// Special operations: the Principal classes delegate their implementation of many of the
// special methods to their underlying StoreCtx
//
// methods for manipulating accounts
/// <summary>
/// This method sets the default user account control bits for the new principal
/// being created in this account store.
/// </summary>
/// <param name="p"> Principal to set the user account control bits for </param>
internal override void InitializeUserAccountControl(AuthenticablePrincipal p)
{
Debug.Assert(p != null);
Debug.Assert(!p.fakePrincipal);
Debug.Assert(p.unpersisted); // should only ever be called for new principals
// set the userAccountControl bits on the underlying directory entry
DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
Debug.Assert(de != null);
Type principalType = p.GetType();
if ((principalType == typeof(ComputerPrincipal)) || (principalType.IsSubclassOf(typeof(ComputerPrincipal))))
{
de.Properties["userAccountControl"].Value = SDSUtils.AD_DefaultUAC_Machine;
}
else if ((principalType == typeof(UserPrincipal)) || (principalType.IsSubclassOf(typeof(UserPrincipal))))
{
de.Properties["userAccountControl"].Value = SDSUtils.AD_DefaultUAC;
}
}
/// <summary>
/// Determine if principal account is locked.
/// First read User-Account-control-computed from the DE. On Uplevel platforms this computed attribute will exist and we can
/// just check bit 0x0010. On DL platforms this attribute does not exist so we must read lockoutTime and return locked if
/// this is greater than 0
/// </summary>
/// <param name="p">Principal to check status</param>
/// <returns>true is account is locked, false if not</returns>
internal override bool IsLockedOut(AuthenticablePrincipal p)
{
try
{
Debug.Assert(!p.fakePrincipal);
Debug.Assert(!p.unpersisted);
DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
Debug.Assert(de != null);
de.RefreshCache(s_msDSUACCLockoutTime);
if (de.Properties["msDS-User-Account-Control-Computed"].Count > 0)
{
// Uplevel platform --- the DC will compute it for us
Debug.Assert(de.Properties["msDS-User-Account-Control-Computed"].Count == 1);
int uacComputed = (int)de.Properties["msDS-User-Account-Control-Computed"][0];
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsLockedOut: using computed uac={0}", uacComputed);
return ((uacComputed & 0x0010) != 0); // UF_LOCKOUT
}
else
{
// Downlevel platform --- we have to compute it
bool isLockedOut = false;
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsLockedOut: downlevel");
if (de.Properties["lockoutTime"].Count > 0)
{
ulong lockoutTime = (ulong)ADUtils.LargeIntToInt64((UnsafeNativeMethods.IADsLargeInteger)de.Properties["lockoutTime"][0]);
if (lockoutTime != 0)
{
ulong lockoutDuration = this.LockoutDuration;
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"IsLockedOut: lockoutTime={0}, lockoutDuration={1}",
lockoutTime,
lockoutDuration);
if ((lockoutDuration + lockoutTime) > ((ulong)ADUtils.DateTimeToADFileTime(DateTime.UtcNow)))
isLockedOut = true;
}
}
return isLockedOut;
}
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
/// <summary>
/// Unlock account by setting LockoutTime to 0
/// </summary>
/// <param name="p">Principal to unlock</param>
internal override void UnlockAccount(AuthenticablePrincipal p)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "UnlockAccount");
Debug.Assert(!p.fakePrincipal);
WriteAttribute(p, "lockoutTime", 0);
}
// methods for manipulating passwords
/// <summary>
/// Set the password on the principal. This function requires administrator privileges
/// </summary>
/// <param name="p">Principal to modify</param>
/// <param name="newPassword">New password</param>
internal override void SetPassword(AuthenticablePrincipal p, string newPassword)
{
Debug.Assert(!p.fakePrincipal);
Debug.Assert(p != null);
Debug.Assert(newPassword != null); // but it could be an empty string
DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
Debug.Assert(de != null);
SDSUtils.SetPassword(de, newPassword);
}
/// <summary>
/// Change the password on the principal
/// </summary>
/// <param name="p">Principal to modify</param>
/// <param name="oldPassword">Current password</param>
/// <param name="newPassword">New password</param>
internal override void ChangePassword(AuthenticablePrincipal p, string oldPassword, string newPassword)
{
Debug.Assert(!p.fakePrincipal);
// Shouldn't be being called if this is the case
Debug.Assert(!p.unpersisted);
Debug.Assert(p != null);
Debug.Assert(newPassword != null); // but it could be an empty string
Debug.Assert(oldPassword != null); // but it could be an empty string
if ((p.GetType() == typeof(ComputerPrincipal)) || (p.GetType().IsSubclassOf(typeof(ComputerPrincipal))))
{
GlobalDebug.WriteLineIf(GlobalDebug.Error, "ADStoreCtx", "ChangePassword: computer acct, can't change password.");
throw new NotSupportedException(SR.ADStoreCtxNoComputerPasswordChange);
}
DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
Debug.Assert(de != null);
SDSUtils.ChangePassword(de, oldPassword, newPassword);
}
/// <summary>
/// Expire password by setting pwdLastSet to 0
/// </summary>
/// <param name="p"></param>
internal override void ExpirePassword(AuthenticablePrincipal p)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExpirePassword");
Debug.Assert(!p.fakePrincipal);
WriteAttribute(p, "pwdLastSet", 0);
}
/// <summary>
/// Unexpire password by setting pwdLastSet to -1
/// </summary>
/// <param name="p"></param>
internal override void UnexpirePassword(AuthenticablePrincipal p)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "UnexpirePassword");
Debug.Assert(!p.fakePrincipal);
WriteAttribute(p, "pwdLastSet", -1);
}
/// <summary>
/// Set value for attribute on the passed principal. This is only valid for integer attribute types
/// </summary>
/// <param name="p"></param>
/// <param name="attribute"></param>
/// <param name="value"></param>
protected void WriteAttribute(Principal p, string attribute, int value)
{
;
Debug.Assert(p != null);
DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
SDSUtils.WriteAttribute(de.Path, attribute, value, this.credentials, this.authTypes);
}
protected void WriteAttribute<T>(Principal p, string attribute, T value)
{
Debug.Assert(p != null);
DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
SDSUtils.WriteAttribute<T>(de.Path, attribute, value, this.credentials, this.authTypes);
}
// the various FindBy* methods
internal override ResultSet FindByLockoutTime(
DateTime dt, MatchType matchType, Type principalType)
{
return FindByDate(principalType, s_lockoutTime, matchType, dt);
}
internal override ResultSet FindByLogonTime(
DateTime dt, MatchType matchType, Type principalType)
{
return FindByDate(principalType, s_lastLogonTime, matchType, dt);
}
internal override ResultSet FindByPasswordSetTime(
DateTime dt, MatchType matchType, Type principalType)
{
return FindByDate(principalType, s_pwdLastSet, matchType, dt);
}
internal override ResultSet FindByBadPasswordAttempt(
DateTime dt, MatchType matchType, Type principalType)
{
return FindByDate(principalType, s_badPasswordTime, matchType, dt);
}
internal override ResultSet FindByExpirationTime(
DateTime dt, MatchType matchType, Type principalType)
{
return FindByDate(principalType, s_accountExpires, matchType, dt);
}
private ADEntriesSet FindByDate(Type subtype, string[] ldapAttributes, MatchType matchType, DateTime value)
{
Debug.Assert(ldapAttributes != null);
Debug.Assert(ldapAttributes.Length > 0);
Debug.Assert(subtype == typeof(Principal) || subtype.IsSubclassOf(typeof(Principal)));
DirectorySearcher ds = new DirectorySearcher(this.ctxBase);
try
{
// Pick some reasonable default values
ds.PageSize = 256;
ds.ServerTimeLimit = new TimeSpan(0, 0, 30); // 30 seconds
// We don't need any attributes returned, since we're just going to get a DirectoryEntry
// for the result. Per RFC 2251, OID 1.1 == no attributes.
BuildPropertySet(subtype, ds.PropertiesToLoad);
// Build the LDAP filter
string ldapValue = ADUtils.DateTimeToADString(value);
StringBuilder ldapFilter = new StringBuilder();
ldapFilter.Append(GetObjectClassPortion(subtype));
ldapFilter.Append("(|");
foreach (string ldapAttribute in ldapAttributes)
{
ldapFilter.Append('(');
switch (matchType)
{
case MatchType.Equals:
ldapFilter.Append(ldapAttribute);
ldapFilter.Append('=');
ldapFilter.Append(ldapValue);
break;
case MatchType.NotEquals:
ldapFilter.Append("!(");
ldapFilter.Append(ldapAttribute);
ldapFilter.Append('=');
ldapFilter.Append(ldapValue);
ldapFilter.Append(')');
break;
case MatchType.GreaterThanOrEquals:
ldapFilter.Append(ldapAttribute);
ldapFilter.Append(">=");
ldapFilter.Append(ldapValue);
break;
case MatchType.LessThanOrEquals:
ldapFilter.Append(ldapAttribute);
ldapFilter.Append("<=");
ldapFilter.Append(ldapValue);
break;
case MatchType.GreaterThan:
ldapFilter.Append('&');
// Greater-than-or-equals (or less-than-or-equals))
ldapFilter.Append('(');
ldapFilter.Append(ldapAttribute);
ldapFilter.Append(matchType == MatchType.GreaterThan ? ">=" : "<=");
ldapFilter.Append(ldapValue);
ldapFilter.Append(')');
// And not-equal
ldapFilter.Append("(!(");
ldapFilter.Append(ldapAttribute);
ldapFilter.Append('=');
ldapFilter.Append(ldapValue);
ldapFilter.Append("))");
// And exists (need to include because of tristate LDAP logic)
ldapFilter.Append('(');
ldapFilter.Append(ldapAttribute);
ldapFilter.Append("=*)");
break;
case MatchType.LessThan:
goto case MatchType.GreaterThan;
default:
Debug.Fail("ADStoreCtx.FindByDate: fell off end looking for " + matchType.ToString());
break;
}
ldapFilter.Append(')');
}
ldapFilter.Append("))");
ds.Filter = ldapFilter.ToString();
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "FindByDate: using LDAP filter {0}", ds.Filter);
// Perform the search
SearchResultCollection src = ds.FindAll();
Debug.Assert(src != null);
// Create a ResultSet for the search results
ADEntriesSet resultSet = new ADEntriesSet(src, this);
return resultSet;
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
finally
{
ds.Dispose();
}
}
// Get groups of which p is a direct member
// search
// 1. search for group with same group id as principals primary group ID.
// Then
// 2. use enumeration to expand the users group membership
// ASQ will not work because we cannot correctly generate referrals if one of the users
// groups if from another domain in the forest.
internal override ResultSet GetGroupsMemberOf(Principal p)
{
// Enforced by the methods that call us
Debug.Assert(!p.unpersisted);
DirectoryEntry gcPrincipalDe = null;
DirectorySearcher memberOfSearcher = null;
ADDNConstraintLinkedAttrSet.ResultValidator resultValidator = null;
try
{
if (p.fakePrincipal)
{
// If p is a fake principal, this will find the representation of p in the store
// (namely, a FPO), and return the groups of which that FPO is a member
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf: fake principal");
return GetGroupsMemberOf(p, this);
}
Debug.Assert(p.UnderlyingObject != null);
string primaryGroupDN = null;
ResultSet resultSet = null;
bool useASQ = false;
List<DirectoryEntry> roots = new List<DirectoryEntry>(1);
DirectorySearcher[] searchers = null;
IEnumerable[] enumerators = null;
DirectoryEntry principalDE = (DirectoryEntry)p.GetUnderlyingObject();
if ((p.ContextType == ContextType.ApplicationDirectory) || (p.Context.ServerInformation.OsVersion == DomainControllerMode.Win2k))
{
useASQ = false;
}
else
{
useASQ = true;
}
if (p.ContextType != ContextType.ApplicationDirectory)
{
// A users group membership that applies to a particular domain includes the domain's universal, local and global groups plus the
// universal groups from every other domain in the forest that the user is a member of. To get this list we must contact both a GlobalCatalog to get the forest
// universal list and a DC in the users domain to get the domain local groups which are not replicated to the GC.
// If we happen to get a GC in the same domain as the user
// then we don't also need a DC because the domain local group memberships will show up as well. The enumerator code that expands these lists must detect
// duplicates because the list of global groups will show up on both the GC and DC.
Debug.Assert(p.ContextType == ContextType.Domain);
Forest forest = Forest.GetForest(new DirectoryContext(DirectoryContextType.Forest, this.DnsForestName, this.credentials?.UserName, this.credentials?.Password));
DirectoryContext dc = new DirectoryContext(DirectoryContextType.Domain, this.DnsDomainName, this.credentials?.UserName, this.credentials?.Password);
DomainController dd = DomainController.FindOne(dc);
GlobalCatalog gc = null;
try
{
gc = forest.FindGlobalCatalog();
var gg = forest.FindAllGlobalCatalogs(dd.SiteName);
foreach (GlobalCatalog g in gg)
{
if (string.Equals(this.DnsDomainName, g.Domain.Name, StringComparison.OrdinalIgnoreCase))
{
gc = g;
break;
}
}
roots.Add(new DirectoryEntry("GC://" + gc.Name + "/" + p.DistinguishedName, this.credentials?.UserName, this.credentials?.Password, this.AuthTypes));
if (!string.Equals(this.DnsDomainName, gc.Domain.Name, StringComparison.OrdinalIgnoreCase))
{
//useASQ = false;
roots.Add(principalDE);
//Since the GC does not belong to the same domain (as the principal object passed)
//We should make sure that we ignore domain local groups that we obtained from the cross-domain GC.
resultValidator = delegate (dSPropertyCollection resultPropCollection)
{
if (resultPropCollection["groupType"].Count > 0 && resultPropCollection["objectSid"].Count > 0)
{
int? groupTypeValue = (int?)resultPropCollection["groupType"][0];
if (groupTypeValue.HasValue && ((groupTypeValue.Value & ADGroupScope.Local) == ADGroupScope.Local))
{
byte[] sidByteArray = (byte[])resultPropCollection["objectSid"][0];
SecurityIdentifier resultSid = new SecurityIdentifier(sidByteArray, 0);
return ADUtils.AreSidsInSameDomain(p.Sid, resultSid);
}
}
return true; //Return true for all other case, including the case where we don't have permissions
//to read groupType/objectSid attribute then we declare the result as a match.
};
}
}
catch (System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException e)
{
// if we can't get a GC then just fail.
throw new PrincipalOperationException(e.Message, e);
}
catch (System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException e)
{
// if we can't get a GC then just fail.
throw new PrincipalOperationException(e.Message, e);
}
finally
{
gc?.Dispose();
forest?.Dispose();
}
}
if (!useASQ)
{
// If this is ADAM then we only need to use the original object.
// IF AD then we will use whatever enumerators we discovered above.
if (p.ContextType != ContextType.ApplicationDirectory)
{
int index = 0;
enumerators = new IEnumerable[roots.Count];
foreach (DirectoryEntry de in roots)
{
//If, de is not equal to principalDE then it must have been created by this function (above code)
//In that case de is NOT owned by any other modules outside. Hence, configure RangeRetriever to dispose the DirEntry on its dispose.
enumerators[index] = new RangeRetriever(de, "memberOf", (de != principalDE));
index++;
}
}
else
{
enumerators = new IEnumerable[1];
//Since principalDE is not owned by us,
//configuring RangeRetriever _NOT_ to dispose the DirEntry on its dispose.
enumerators[0] = new RangeRetriever(principalDE, "memberOf", false);
}
}
else
{
int index = 0;
searchers = new DirectorySearcher[roots.Count];
foreach (DirectoryEntry de in roots)
{
searchers[index] = SDSUtils.ConstructSearcher(de);
searchers[index].SearchScope = SearchScope.Base;
searchers[index].AttributeScopeQuery = "memberOf";
searchers[index].Filter = "(objectClass=*)";
searchers[index].CacheResults = false;
BuildPropertySet(typeof(GroupPrincipal), searchers[index].PropertiesToLoad);
index++;
}
}
string principalDN = (string)principalDE.Properties["distinguishedName"].Value;
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf: principalDN={0}", principalDN);
principalDE.RefreshCache(s_memberOfPrimaryGroupId);
if ((principalDE.Properties["primaryGroupID"].Count > 0) &&
(principalDE.Properties["objectSid"].Count > 0))
{
Debug.Assert(principalDE.Properties["primaryGroupID"].Count == 1);
Debug.Assert(principalDE.Properties["objectSid"].Count == 1);
int primaryGroupID = (int)principalDE.Properties["primaryGroupID"].Value;
byte[] principalSid = (byte[])principalDE.Properties["objectSid"].Value;
primaryGroupDN = GetGroupDnFromGroupID(principalSid, primaryGroupID);
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf: primary group DN={0}", primaryGroupDN);
}
// We must use enumeration to expand the users group membership
// ASQ will not work because we cannot correctly generate referrals if one of the users
// groups if from another domain in the forest.
if (useASQ)
{
if (resultValidator != null)
{
resultSet = new ADDNConstraintLinkedAttrSet(
ADDNConstraintLinkedAttrSet.ConstraintType.ResultValidatorDelegateMatch,
resultValidator, principalDN, searchers, primaryGroupDN, null, false, this);
}
else
{
resultSet = new ADDNLinkedAttrSet(principalDN, searchers, primaryGroupDN, null, false, this);
}
}
else
{
if (resultValidator != null)
{
resultSet = new ADDNConstraintLinkedAttrSet(
ADDNConstraintLinkedAttrSet.ConstraintType.ResultValidatorDelegateMatch,
resultValidator, principalDN, enumerators, primaryGroupDN, null, false, this);
}
else
{
resultSet = new ADDNLinkedAttrSet(principalDN, enumerators, primaryGroupDN, null, false, this);
}
}
return resultSet;
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
finally
{
gcPrincipalDe?.Dispose();
memberOfSearcher?.Dispose();
}
}
// Get groups from this ctx which contain a principal corresponding to foreignPrincipal
// (which is a principal from foreignContext)
internal override ResultSet GetGroupsMemberOf(Principal foreignPrincipal, StoreCtx foreignContext)
{
// Get the Principal's SID, so we can look it up by SID in our store
SecurityIdentifier Sid = foreignPrincipal.Sid;
if (Sid == null)
throw new InvalidOperationException(SR.StoreCtxNeedValueSecurityIdentityClaimToQuery);
// Search our store for a object with a matching SID. This could be a user/group/computer object,
// or a foreignSecurityPrincipal. Doesn't really matter --- either way, the store object will have a objectSid
// and a memberOf attribute.
// SID search
//
//
// If we can read the defaultNamingContext and retrieve the well known path for the foreignSecurityPrincipal container start there.
// If we can only read the defaultNamingContext then start there
// Else just start at the base DN from the original context
//
// If the object was not found and we started at teh fsp container then search the entire DC.
// Else just exit. we have nothing else to search
// An object exists in the domain that contains links to all the groups it is a member of.
bool rootPrincipalExists = true;
if ((foreignContext is ADStoreCtx) && !(foreignContext is ADAMStoreCtx))
{
// We only need to check forest status for AD stores. Forest concept does not apply to ADAM.
ADStoreCtx foreignADStore = (ADStoreCtx)foreignContext;
// If same forest but different domain then we have a child or alternate tree domain. We don't have a starting user
// object and must do a search on all groups to find membership.
if (string.Equals(foreignADStore.DnsForestName, this.DnsForestName, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(foreignADStore.DnsDomainName, this.DnsDomainName, StringComparison.OrdinalIgnoreCase))
{
rootPrincipalExists = true;
}
else
{
rootPrincipalExists = false;
}
}
}
DirectoryEntry dncContainer = null;
string fspWkDn = null;
DirectoryEntry fspContainer = null;
ResultSet resultSet = null;
DirectorySearcher ds = null;
try
{
if (rootPrincipalExists)
{
if (this.DefaultNamingContext != null)
{
dncContainer = new DirectoryEntry(@"LDAP://" + this.UserSuppliedServerName + @"/" + this.DefaultNamingContext, Credentials != null ? this.Credentials.UserName : null, Credentials != null ? this.Credentials.Password : null, this.AuthTypes);
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf(ctx): Read DNC of {0}", this.DefaultNamingContext);
fspWkDn = ADUtils.RetrieveWkDn(dncContainer, this.DefaultNamingContext, this.UserSuppliedServerName, Constants.GUID_FOREIGNSECURITYPRINCIPALS_CONTAINER_BYTE);
if (null != fspWkDn)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf(ctx): Read fsp DN {0}", fspWkDn);
fspContainer = new DirectoryEntry(fspWkDn, Credentials != null ? this.credentials.UserName : null, Credentials != null ? this.credentials.Password : null, this.authTypes);
}
}
ds = new DirectorySearcher(fspContainer ?? dncContainer ?? this.ctxBase);
// Pick some reasonable default values
ds.PageSize = 256;
ds.ServerTimeLimit = new TimeSpan(0, 0, 30); // 30 seconds
// Build the LDAP filter
// Converr the object to a SDDL format
string stringSid = Utils.SecurityIdentifierToLdapHexFilterString(Sid);
if (stringSid == null)
throw new InvalidOperationException(SR.StoreCtxNeedValueSecurityIdentityClaimToQuery);
ds.Filter = "(objectSid=" + stringSid + ")";
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf(ctx): using LDAP filter {0}", ds.Filter);
// We only need a few attributes
ds.PropertiesToLoad.Add("memberOf");
ds.PropertiesToLoad.Add("distinguishedName");
ds.PropertiesToLoad.Add("primaryGroupID");
ds.PropertiesToLoad.Add("objectSid");
// If no corresponding principal exists in this store, then by definition the principal isn't
// a member of any groups in this store.
SearchResult sr = ds.FindOne();
if (sr == null)
{
// no match so we better do a root level search in case we are targeting a domain where
// the user is not an FSP.
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf(ctx): No match");
// We already did a root level search so just exit.
if (null == fspWkDn)
return new EmptySet();
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf(ctx): performing DNC level search");
ds.SearchRoot = dncContainer;
sr = ds.FindOne();
if (sr == null)
return new EmptySet();
}
// Now that we found the corresponding principal, the rest is very similar to the plain GetGroupsMemberOf()
// case, exception we're working with search results (SearchResult/ResultPropertyValueCollection) rather
// than DirectoryEntry/PropertyValueCollection.
string principalDN = (string)sr.Properties["distinguishedName"][0];
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf(ctx): match, DN={0}", principalDN);
//Here a new DirectoryEntry object is created by sr.GetDirectoryEntry() and passed
//to RangeRetriever object. Hence, configuring RangeRetriever to dispose the DirEntry on its dispose.
IEnumerable memberOf = new RangeRetriever(sr.GetDirectoryEntry(), "memberOf", true);
string primaryGroupDN = null;
if ((sr.Properties["primaryGroupID"].Count > 0) &&
(sr.Properties["objectSid"].Count > 0))
{
Debug.Assert(sr.Properties["primaryGroupID"].Count == 1);
Debug.Assert(sr.Properties["objectSid"].Count == 1);
int primaryGroupID = (int)sr.Properties["primaryGroupID"][0];
byte[] principalSid = (byte[])sr.Properties["objectSid"][0];
primaryGroupDN = GetGroupDnFromGroupID(principalSid, primaryGroupID);
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupsMemberOf(ctx): primary group DN={0}", primaryGroupDN);
}
resultSet = new ADDNConstraintLinkedAttrSet(ADDNConstraintLinkedAttrSet.ConstraintType.ContainerStringMatch, this.ctxBase.Properties["distinguishedName"].Value, principalDN, new IEnumerable[] { memberOf }, primaryGroupDN, null, false, this);
}
else
{
// We don't need to retrieve the Primary group ID here because we have already established that this user is not from this domain
// and the users primary group must be from the same domain as the user.
Debug.Assert(foreignPrincipal.ContextType != ContextType.ApplicationDirectory);
DirectorySearcher[] memberSearcher = { SDSUtils.ConstructSearcher(this.ctxBase) };
memberSearcher[0].Filter = "(&(objectClass=Group)(member=" + ADUtils.EscapeRFC2254SpecialChars(foreignPrincipal.DistinguishedName) + "))";
memberSearcher[0].CacheResults = false;
resultSet = new ADDNLinkedAttrSet(foreignPrincipal.DistinguishedName, memberSearcher, null, null, false, this);
}
return resultSet;
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
finally
{
fspContainer?.Dispose();
ds?.Dispose();
dncContainer?.Dispose();
}
}
private unsafe string GetGroupDnFromGroupID(byte[] userSid, int primaryGroupId)
{
void* pGroupSid = null;
byte[] groupSid = null;
// This function is based on the technique in KB article 297951.
try
{
string sddlSid = Utils.ConvertSidToSDDL(userSid);
if (sddlSid != null)
{
// Next, we modify the SDDL SID to replace with final subauthority
// with the primary group's RID (the primaryGroupID)
int index = sddlSid.LastIndexOf('-');
if (index != -1)
{
sddlSid = sddlSid.Substring(0, index) + "-" + ((uint)primaryGroupId).ToString(CultureInfo.InvariantCulture);
// Now, we convert the SDDL back into a SID
if (Interop.Advapi32.ConvertStringSidToSid(sddlSid, out pGroupSid) != Interop.BOOL.FALSE)
{
// Now we convert the native SID to a byte[] SID
groupSid = Utils.ConvertNativeSidToByteArray((IntPtr)pGroupSid);
}
}
}
}
finally
{
if (pGroupSid is not null)
Interop.Kernel32.LocalFree(pGroupSid);
}
if (groupSid != null)
{
return "<SID=" + Utils.ByteArrayToString(groupSid) + ">";
}
return null;
}
// Get groups of which p is a member, using AuthZ S4U APIs for recursive membership
internal override ResultSet GetGroupsMemberOfAZ(Principal p)
{
// Enforced by the methods that call us
Debug.Assert(!p.unpersisted);
Debug.Assert(p is UserPrincipal);
// Get the user SID that AuthZ will use.
SecurityIdentifier SidObj = p.Sid;
if (SidObj == null)
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "GetGroupsMemberOfAZ: no SID IC");
throw new InvalidOperationException(SR.StoreCtxNeedValueSecurityIdentityClaimToQuery);
}
byte[] sid = new byte[SidObj.BinaryLength];
SidObj.GetBinaryForm(sid, 0);
if (sid == null)
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "GetGroupsMemberOfAZ: bad SID IC");
throw new ArgumentException(SR.StoreCtxSecurityIdentityClaimBadFormat);
}
try
{
if (ADUtils.VerifyOutboundTrust(this.DnsDomainName, this.credentials?.UserName, this.credentials?.Password))
{
return new AuthZSet(sid, this.credentials, this.contextOptions, this.FlatDomainName, this, this.ctxBase);
}
else
{
DirectoryEntry principalDE = (DirectoryEntry)p.UnderlyingObject;
string principalDN = (string)principalDE.Properties["distinguishedName"].Value;
return (new TokenGroupSet(principalDN, this, true));
}
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
// Get members of group g
// Need 2 searchers
// 1. Users with this group as their primary group ID
// 2. ASQ search against the member attribute on the group object for all contained objects.
internal override BookmarkableResultSet GetGroupMembership(GroupPrincipal g, bool recursive)
{
// Enforced by the methods that call us
Debug.Assert(!g.unpersisted);
// Fake groups are a member of other groups, but they themselves have no members
// (they don't even exist in the store)
if (g.fakePrincipal)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupMembership: fake principal");
return new EmptySet();
}
Debug.Assert(g.UnderlyingObject != null);
try
{
DirectoryEntry groupDE = (DirectoryEntry)g.UnderlyingObject;
// Create a DirectorySearcher for users who are members of this group via their primaryGroupId attribute
DirectorySearcher ds = null;
if (groupDE.Properties["objectSid"].Count > 0)
{
Debug.Assert(groupDE.Properties["objectSid"].Count == 1);
byte[] groupSid = (byte[])groupDE.Properties["objectSid"][0];
ds = GetDirectorySearcherFromGroupID(groupSid);
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupMembership: using LDAP filter={0}", ds.Filter);
}
string groupDN = (string)groupDE.Properties["distinguishedName"].Value;
BookmarkableResultSet resultSet = null;
// We must use enumeration to expand groups if their scope is Universal or Local
// or if the domain controller is w2k
// or if the context type is ApplicationDirectory (in AD LDS, Global groups can contain members from other partition)
// Universal and Local groups can contain members from other domains in the forest. When this occurs
// the referral is not generated correctly and we get an error.
//
if (g.Context.ContextType == ContextType.ApplicationDirectory ||
g.Context.ServerInformation.OsVersion == DomainControllerMode.Win2k ||
g.GroupScope != GroupScope.Global)
{
//Here the directory entry passed to RangeRetriever constructor belongs to
//the GroupPrincipal object supplied to this function, which is not owned by us.
//Hence, configuring RangeRetriever _NOT_ to dispose the DirEntry on its dispose.
IEnumerable members = new RangeRetriever(groupDE, "member", false);
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetGroupMembership: groupDN={0}", groupDN);
resultSet = new ADDNLinkedAttrSet(groupDN, new IEnumerable[] { members }, null, ds, recursive, this);
}
else
{
DirectorySearcher[] dsMembers = new DirectorySearcher[1];
dsMembers[0] = SDSUtils.ConstructSearcher((DirectoryEntry)g.UnderlyingObject);
dsMembers[0].AttributeScopeQuery = "member";
dsMembers[0].SearchScope = SearchScope.Base;
dsMembers[0].Filter = "(objectClass=*)";
dsMembers[0].CacheResults = false;
BuildPropertySet(typeof(UserPrincipal), dsMembers[0].PropertiesToLoad);
BuildPropertySet(typeof(GroupPrincipal), dsMembers[0].PropertiesToLoad);
resultSet = new ADDNLinkedAttrSet(groupDN, dsMembers, null, ds, recursive, this);
}
return resultSet;
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
private DirectorySearcher GetDirectorySearcherFromGroupID(byte[] groupSid)
{
Debug.Assert(groupSid != null);
// Get the group's RID from the group's SID
int groupRid = Utils.GetLastRidFromSid(groupSid);
// Build a DirectorySearcher for users whose primaryGroupId == the group's RID
DirectorySearcher ds = new DirectorySearcher(this.ctxBase);
ds.Filter = GetObjectClassPortion(typeof(Principal)) + "(primaryGroupId=" + groupRid.ToString(CultureInfo.InvariantCulture) + "))";
// Pick some reasonable default values
ds.PageSize = 256;
ds.ServerTimeLimit = new TimeSpan(0, 0, 30); // 30 seconds
BuildPropertySet(typeof(Principal), ds.PropertiesToLoad);
return ds;
}
// Is p a member of g in the store?
internal override bool SupportsNativeMembershipTest { get { return true; } }
/// First check direct group membership by using DE.IsMember
/// If this fails then we may have a ForeignSecurityPrincipal so search for Foreign Security Principals
/// With the p's SID and then call IsMember with the ADS Path returned from the search.
internal override bool IsMemberOfInStore(GroupPrincipal g, Principal p)
{
Debug.Assert(!g.unpersisted);
Debug.Assert(!p.unpersisted);
// Consistent with GetGroupMembership, a group that is a fake principal has no members
if (g.fakePrincipal)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsMemberOfInStore: fake group");
return false;
}
// AD Groups can only have AD principals as members
if (p.ContextType != ContextType.Domain && p.ContextType != ContextType.ApplicationDirectory)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsMemberOfInStore: member is not a domain principal");
return false;
}
Debug.Assert(g.UnderlyingObject != null && g.UnderlyingObject is DirectoryEntry);
IEnumerable cachedMembersEnum = null; //This variables stores a reference to the direct members enumerator of the group.
// Only real principals can be directly a member of the group, since only real principals
// actually exist in the store.
if (!p.fakePrincipal)
{
Debug.Assert(p.UnderlyingObject != null && p.UnderlyingObject is DirectoryEntry);
//// Test for direct membership
////
DirectoryEntry principalDE = (DirectoryEntry)p.UnderlyingObject;
DirectoryEntry groupDE = (DirectoryEntry)g.UnderlyingObject;
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsMemberOfInStore: real principal, DN={0}", principalDE.Path);
string principalDN = (string)principalDE.Properties["distinguishedName"].Value;
// we want to find if a group is "small", meaning that it has less than MaxValRange values (usually 1500)
// the property list for the searcher of a group has "member" attribute. if there are more results than MaxValRange, there will also be a "member;range=..." attribute
if (g.IsSmallGroup())
{
// small groups has special search object that holds the member attribute so we use it for our search (no need to use the DirectoryEntry)
Debug.Assert(g.SmallGroupMemberSearchResult != null);
cachedMembersEnum = g.SmallGroupMemberSearchResult.Properties["member"];
if ((g.SmallGroupMemberSearchResult != null) && g.SmallGroupMemberSearchResult.Properties["member"].Contains(principalDN))
{
return true;
}
}
else
{
// this is a large group. use range retrieval instead of simple attribute check.
RangeRetriever rangeRetriever = new RangeRetriever(groupDE, "member", false);
rangeRetriever.CacheValues = true;
foreach (string memberDN in rangeRetriever)
{
if (principalDN.Equals(memberDN, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
rangeRetriever.Reset(); //Reset range-retriever enum, so that it can be traversed again.
cachedMembersEnum = rangeRetriever;
}
}
//
// Might be an FPO (either a real principal from another forest, or a fake principal
// that AD represents as an FPO). Search for the FPO, and test its DN for membership.
//
// Get the Principal's SID, so we can look it up by SID in our store
SecurityIdentifier Sid = p.Sid;
if (Sid == null)
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "IsMemberOfInStore: no SID IC or null UrnValue");
throw new ArgumentException(SR.StoreCtxNeedValueSecurityIdentityClaimToQuery);
}
DirectoryEntry defaultNCDirEntry = null;
DirectorySearcher ds = null;
try
{
string path = $"LDAP://{(string.IsNullOrEmpty(this.UserSuppliedServerName) ? this.DnsHostName : this.UserSuppliedServerName)}/{this.ContextBasePartitionDN}";
defaultNCDirEntry = SDSUtils.BuildDirectoryEntry(path, this.credentials, this.authTypes);
ds = new DirectorySearcher(defaultNCDirEntry);
// Pick some reasonable default values
ds.ServerTimeLimit = new TimeSpan(0, 0, 30); // 30 seconds
// Build the LDAP filter, Convert the sid to SDDL format
string stringSid = Utils.SecurityIdentifierToLdapHexFilterString(Sid);
if (stringSid == null)
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "IsMemberOfInStore: bad SID IC");
throw new ArgumentException(SR.StoreCtxNeedValueSecurityIdentityClaimToQuery);
}
ds.Filter = "(&(objectClass=foreignSecurityPrincipal)(objectSid=" + stringSid + "))";
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"IsMemberOfInStore: FPO principal, using LDAP filter {0}",
ds.Filter);
ds.PropertiesToLoad.Add("distinguishedName");
SearchResult sr = ds.FindOne();
// No FPO ---> not a member
if (sr == null)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsMemberOfInStore: no FPO found");
return false;
}
string fpoDN = (string)sr.Properties["distinguishedName"][0];
foreach (string memberDN in cachedMembersEnum)
{
if (string.Equals(fpoDN, memberDN, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
finally
{
ds?.Dispose();
defaultNCDirEntry?.Dispose();
}
}
// The only reason a Clear() operation can not be performed on the group is if there
// are one or more members on the store who are a member of the group by virtue of their
// primaryGroupId, rather than by the group's "member" attribute.
internal override bool CanGroupBeCleared(GroupPrincipal g, out string explanationForFailure)
{
explanationForFailure = null;
// If the group is unpersisted, there are certainly no principals in the store who
// are a member of it by vitue of primaryGroupId. If the group is a fake group, it has no
// members. Either way, they can clear it.
if (g.unpersisted || g.fakePrincipal)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"CanGroupBeCleared: unpersisted={0}, fake={1}",
g.unpersisted,
g.fakePrincipal);
return true;
}
Debug.Assert(g.UnderlyingObject != null);
DirectoryEntry groupDE = (DirectoryEntry)g.UnderlyingObject;
// Create a DirectorySearcher for users who are members of this group via their primaryGroupId attribute
DirectorySearcher ds = null;
try
{
if (groupDE.Properties["objectSid"].Count > 0)
{
Debug.Assert(groupDE.Properties["objectSid"].Count == 1);
byte[] groupSid = (byte[])groupDE.Properties["objectSid"][0];
ds = GetDirectorySearcherFromGroupID(groupSid);
// We only need to know if there's at least one such user
ds.SizeLimit = 1;
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsMemberOfInStore: using LDAP filter {0}", ds.Filter);
SearchResult sr = ds.FindOne();
if (sr != null)
{
// there is such a member, we can't clear the group
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsMemberOfInStore: found member, can't clear");
explanationForFailure = SR.ADStoreCtxCantClearGroup;
return false;
}
else
{
// no such members, we can clear the group
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "IsMemberOfInStore: no member, can clear");
return true;
}
}
else
{
// We don't have sufficient information. Assume we can clear it.
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "IsMemberOfInStore: can't search, assume can clear");
return true;
}
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
finally
{
ds?.Dispose();
}
}
// The only reason we wouldn't be able to remove this member is if it's a member by virtue of its
// primaryGroupId rather than the group's "member" attribute
internal override bool CanGroupMemberBeRemoved(GroupPrincipal g, Principal member, out string explanationForFailure)
{
explanationForFailure = null;
// If the member is unpersisted, it has no primaryGroupId attribute that could point to the group.
// If the member is a fake princiapl, it also has no primaryGroupId attribute that could point to the group.
// So either way, we have no objections to it being removed from the group.
if (member.unpersisted || member.fakePrincipal)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"CanGroupMemberBeRemoved: member unpersisted={0}, fake={1}",
member.unpersisted,
member.fakePrincipal);
return true;
}
// If the group is unpersisted, then clearly there is no way the principal is
// a member of it by vitue of primaryGroupId. If the group is a fake group, it has no
// members and so we don't care about it.
if (g.unpersisted || g.fakePrincipal)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"CanGroupMemberBeRemoved: group unpersisted={0}, fake={1}",
g.unpersisted,
g.fakePrincipal);
return true;
}
// ADAM groups can have AD or ADAM members, AD groups can only have AD members
//, but we could be called before our caller
// has verified the principal being passed in as the member. Just ignore it if the member isn't an AD principal,
// it'll be caught later in PrincipalCollection.Remove().
if ((g.ContextType == ContextType.Domain && member.ContextType != ContextType.Domain) ||
(member.ContextType != ContextType.Domain && member.ContextType != ContextType.ApplicationDirectory))
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "CanGroupMemberBeRemoved: member is not a domain or application directory principal");
return true;
}
try
{
Debug.Assert(g.UnderlyingObject != null);
Debug.Assert(member.UnderlyingObject != null);
DirectoryEntry groupDE = (DirectoryEntry)g.UnderlyingObject;
DirectoryEntry memberDE = (DirectoryEntry)member.UnderlyingObject;
if ((groupDE.Properties["objectSid"].Count > 0) &&
(memberDE.Properties["primaryGroupID"].Count > 0))
{
Debug.Assert(groupDE.Properties["objectSid"].Count == 1);
Debug.Assert(memberDE.Properties["primaryGroupID"].Count == 1);
byte[] groupSid = (byte[])groupDE.Properties["objectSid"][0];
int primaryGroupID = (int)memberDE.Properties["primaryGroupID"][0];
int groupRid = Utils.GetLastRidFromSid(groupSid);
if (groupRid == primaryGroupID)
{
// It is a primary group member, we can't remove it.
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"CanGroupMemberBeRemoved: primary group member (rid={0}), can't remove",
groupRid);
explanationForFailure = SR.ADStoreCtxCantRemoveMemberFromGroup;
return false;
}
else
{
// It's not a primary group member, we can remove it.
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"CanGroupMemberBeRemoved: not primary group member (group rid={0}, primary group={1}), can remove",
groupRid,
primaryGroupID);
return true;
}
}
else
{
// We don't have sufficient information. Assume we can remove it.
// If we can't, we'll get an exception when we try to save the changes.
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "CanGroupMemberBeRemoved: can't test, assume can remove");
return true;
}
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
//
// Cross-store support
//
// Given a native store object that represents a "foreign" principal (e.g., a FPO object in this store that
// represents a pointer to another store), maps that representation to the other store's StoreCtx and returns
// a Principal from that other StoreCtx. The implementation of this method is highly dependent on the
// details of the particular store, and must have knowledge not only of this StoreCtx, but also of how to
// interact with other StoreCtxs to fulfill the request.
//
// This method is typically used by ResultSet implementations, when they're iterating over a collection
// (e.g., of group membership) and encounter an entry that represents a foreign principal.
internal override Principal ResolveCrossStoreRefToPrincipal(object o)
{
Debug.Assert(o is DirectoryEntry);
Debug.Assert(ADUtils.IsOfObjectClass((DirectoryEntry)o, "foreignSecurityPrincipal"));
try
{
// Get the SID of the foreign principal
DirectoryEntry fpoDE = (DirectoryEntry)o;
if (fpoDE.Properties["objectSid"].Count == 0)
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "ResolveCrossStoreRefToPrincipal: no objectSid found");
throw new PrincipalOperationException(SR.ADStoreCtxCantRetrieveObjectSidForCrossStore);
}
Debug.Assert(fpoDE.Properties["objectSid"].Count == 1);
byte[] sid = (byte[])fpoDE.Properties["objectSid"].Value;
// What type of SID is it?
SidType sidType = Utils.ClassifySID(sid);
if (sidType == SidType.FakeObject)
{
// It's a FPO for something like NT AUTHORITY\NETWORK SERVICE.
// There's no real store object corresponding to this FPO, so
// fake a Principal.
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"ResolveCrossStoreRefToPrincipal: fake principal, SID={0}",
Utils.ByteArrayToString(sid));
return ConstructFakePrincipalFromSID(sid);
}
StoreCtx foreignStoreCtx;
if (sidType == SidType.RealObjectFakeDomain)
{
// This is a BUILTIN object. It's a real object on the store we're connected to, but LookupSid
// will tell us it's a member of the BUILTIN domain. Resolve it as a principal on our store.
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "ResolveCrossStoreRefToPrincipal: builtin principal");
foreignStoreCtx = this;
}
else
{
// Ask the OS to resolve the SID to its target.
UnsafeNativeMethods.IAdsObjectOptions objOptions = (UnsafeNativeMethods.IAdsObjectOptions)this.ctxBase.NativeObject;
string serverName = (string)objOptions.GetOption(0 /* == ADS_OPTION_SERVERNAME */);
int accountUsage = 0;
string name;
string domainName;
int err = Utils.LookupSid(serverName, this.credentials, sid, out name, out domainName, out accountUsage);
if (err != 0)
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn,
"ADStoreCtx",
"ResolveCrossStoreRefToPrincipal: LookupSid failed, err={0}, server={1}",
err,
serverName);
throw new PrincipalOperationException(
SR.Format(SR.ADStoreCtxCantResolveSidForCrossStore, err));
}
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"ResolveCrossStoreRefToPrincipal: LookupSid found {0} in {1}",
name,
domainName);
// Since this is AD, the remote principal must be an AD principal.
// Build a PrincipalContext for the store which owns the principal
// We are now connecting to AD so change to negotiate with signing and sealing
ContextOptions remoteOptions = DefaultContextOptions.ADDefaultContextOption;
PrincipalContext remoteCtx = SDSCache.Domain.GetContext(domainName, this.credentials, remoteOptions);
foreignStoreCtx = remoteCtx.QueryCtx;
}
Principal p = foreignStoreCtx.FindPrincipalByIdentRef(
typeof(Principal),
UrnScheme.SidScheme,
(new SecurityIdentifier(sid, 0)).ToString(),
DateTime.UtcNow);
if (p != null)
return p;
else
{
GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "ResolveCrossStoreRefToPrincipal: no matching principal");
throw new PrincipalOperationException(SR.ADStoreCtxFailedFindCrossStoreTarget);
}
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
// Returns true if AccountInfo is supported for the specified principal, false otherwise.
// Used when an application tries to access the AccountInfo property of a newly-inserted
// (not yet persisted) AuthenticablePrincipal, to determine whether it should be allowed.
internal override bool SupportsAccounts(AuthenticablePrincipal p)
{
// Fake principals do not have store objects, so they certainly don't have stored account info.
if (p.fakePrincipal)
return false;
// Computer is a subclass of user in AD, and both therefore support account info.
return true;
}
// Returns the set of credential types supported by this store for the specified principal.
// Used when an application tries to access the PasswordInfo property of a newly-inserted
// (not yet persisted) AuthenticablePrincipal, to determine whether it should be allowed.
// Also used to implement AuthenticablePrincipal.SupportedCredentialTypes.
internal override CredentialTypes SupportedCredTypes(AuthenticablePrincipal p)
{
// Fake principals do not have store objects, so they certainly don't have stored creds.
if (p.fakePrincipal)
return (CredentialTypes)0;
return CredentialTypes.Password | CredentialTypes.Certificate;
}
//
// When called, this function does a GetInfoEx() to preload the DirectoryEntry's
// property cache with all the attributes we'll be using. This avoids DirectoryEntry
// doing an implicit GetInfo() and pulling down every attribute.
//
// This function is currently loading every attribute from the directory instead of using the known list.
internal void LoadDirectoryEntryAttributes(DirectoryEntry de)
{
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "LoadDirectoryEntryAttributes, path={0}", de.Path);
try
{
// de.RefreshCache(ldapAttributesUsed);
de.RefreshCache();
}
catch (System.Runtime.InteropServices.COMException e)
{
throw ExceptionHelper.GetExceptionFromCOMException(e);
}
}
//
// Construct a fake Principal to represent a well-known SID like
// "\Everyone" or "NT AUTHORITY\NETWORK SERVICE"
//
internal override Principal ConstructFakePrincipalFromSID(byte[] sid)
{
UnsafeNativeMethods.IAdsObjectOptions objOptions = (UnsafeNativeMethods.IAdsObjectOptions)this.ctxBase.NativeObject;
string serverName = (string)objOptions.GetOption(0 /* == ADS_OPTION_SERVERNAME */);
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"ConstructFakePrincipalFromSID: sid={0}, server={1}, authority={2}",
Utils.ByteArrayToString(sid),
serverName,
this.DnsDomainName);
Principal p = Utils.ConstructFakePrincipalFromSID(
sid,
this.OwningContext,
serverName,
this.credentials,
this.DnsDomainName);
// Assign it a StoreKey
ADStoreKey key = new ADStoreKey(this.DnsDomainName, sid);
p.Key = key;
return p;
}
//
// Private data
//
///
/// <summary>
/// Returns the DN of the Partition to which the user supplied
/// context base (this.ctxBase) belongs.
/// </summary>
///
internal string ContextBasePartitionDN
{
get
{
if (this.contextBasePartitionDN == null)
{
lock (this.domainInfoLock)
{
if (contextBasePartitionDN == null)
LoadDomainInfo();
}
}
return this.contextBasePartitionDN;
}
}
internal string DefaultNamingContext
{
get
{
if (this.defaultNamingContext == null)
{
lock (this.domainInfoLock)
{
if (defaultNamingContext == null)
LoadDomainInfo();
}
}
return this.defaultNamingContext;
}
}
private string FlatDomainName
{
get
{
if (this.domainFlatName == null)
{
lock (this.domainInfoLock)
{
if (domainFlatName == null)
LoadDomainInfo();
}
}
return this.domainFlatName;
}
}
internal string DnsDomainName
{
get
{
if (this.domainDnsName == null)
{
lock (this.domainInfoLock)
{
if (domainDnsName == null)
LoadDomainInfo();
}
}
return this.domainDnsName;
}
}
internal string DnsHostName
{
get
{
if (this.dnsHostName == null)
{
lock (this.domainInfoLock)
{
if (dnsHostName == null)
LoadDomainInfo();
}
}
return this.dnsHostName;
}
}
internal string DnsForestName
{
get
{
if (this.forestDnsName == null)
{
lock (this.domainInfoLock)
{
if (forestDnsName == null)
LoadDomainInfo();
}
}
return this.forestDnsName;
}
}
internal string UserSuppliedServerName
{
get
{
if (this.userSuppliedServerName == null)
{
lock (this.domainInfoLock)
{
if (this.userSuppliedServerName == null)
LoadDomainInfo();
}
}
return this.userSuppliedServerName;
}
}
private ulong LockoutDuration
{
get
{
// We test domainDnsName for null because lockoutDuration could legitimately be 0,
// but lockoutDuration is valid iff domainDnsName is non-null
if (this.domainDnsName == null)
{
lock (this.domainInfoLock)
{
if (domainDnsName == null)
LoadDomainInfo();
}
}
return this.lockoutDuration;
}
}
protected object domainInfoLock = new object();
protected string domainFlatName;
protected string domainDnsName;
protected string forestDnsName;
protected string userSuppliedServerName;
protected string defaultNamingContext;
protected string contextBasePartitionDN; //contains the DN of the Partition to which the user supplied context base (this.ctxBase) belongs.
protected string dnsHostName;
protected ulong lockoutDuration;
private static readonly string[] s_lockoutTime = new string[] { "lockoutTime" };
private static readonly string[] s_lastLogonTime = new string[] { "lastLogon", "lastLogonTimestamp" };
private static readonly string[] s_pwdLastSet = new string[] { "pwdLastSet" };
private static readonly string[] s_badPasswordTime = new string[] { "badPasswordTime" };
private static readonly string[] s_accountExpires = new string[] { "accountExpires" };
private static readonly string[] s_nTSecurityDescriptor = new string[] { "nTSecurityDescriptor" };
private static readonly string[] s_msDSUACCLockoutTime = new string[] { "msDS-User-Account-Control-Computed", "lockoutTime" };
private static readonly string[] s_memberOfPrimaryGroupId = new string[] { "memberOf", "primaryGroupID" };
private static readonly string[] s_lockoutDuration = new string[] { "lockoutDuration" };
internal static readonly char[] s_comma = new char[] { ',' };
protected enum StoreCapabilityMap
{
ASQSearch = 1,
}
// Must be called inside of lock(domainInfoLock)
protected virtual void LoadDomainInfo()
{
const int LdapDefaultPort = 389;
const int LdapsDefaultPort = 636;
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "LoadComputerInfo");
Debug.Assert(this.ctxBase != null);
//
// DNS Domain Name
//
// We need to connect to the server's rootDse to get the naming context
// From that, we can build the DNS Domain Name
this.dnsHostName = ADUtils.GetServerName(this.ctxBase);
// Pull the requested port number
int port = LdapDefaultPort;
if (Uri.TryCreate(ctxBase.Path, UriKind.Absolute, out Uri ldapUri))
{
if (ldapUri.Port != -1)
{
port = ldapUri.Port;
}
else if (string.Equals(ldapUri.Scheme, "LDAPS", StringComparison.OrdinalIgnoreCase))
{
port = LdapsDefaultPort;
}
}
string dnsDomainName = "";
using (DirectoryEntry rootDse = new DirectoryEntry($"LDAP://{this.dnsHostName}:{port}/rootDse", "", "", AuthenticationTypes.Anonymous))
{
this.defaultNamingContext = (string)rootDse.Properties["defaultNamingContext"][0];
this.contextBasePartitionDN = this.defaultNamingContext;
// Split the naming context's DN into its RDNs
string[] ncComponents = defaultNamingContext.Split(s_comma);
StringBuilder sb = new StringBuilder();
// Reassemble the RDNs (minus the tag) into the DNS domain name
foreach (string component in ncComponents)
{
// If it's not a "DC=" component, skip it
if ((component.Length > 3) &&
string.Equals(component.Substring(0, 3), "DC=", StringComparison.OrdinalIgnoreCase))
{
sb.Append(component, 3, component.Length - 3);
sb.Append('.');
}
}
dnsDomainName = sb.ToString();
// The loop added an extra period at the end. Remove it.
if (dnsDomainName.Length > 0)
dnsDomainName = dnsDomainName.Substring(0, dnsDomainName.Length - 1);
this.domainDnsName = dnsDomainName;
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "LoadComputerInfo: using DNS domain name {0}", dnsDomainName);
}
//
// NetBios (flat) Domain Name, and DNS Forest Name
//
// Given the DNS domain name, we use DsGetDcName to get the flat name.
// The same DsGetDcName call also retrieves the DNS forest name as a side effect.
//
// DS_IS_DNS_NAME | DS_RETURN_FLAT_NAME | DS_DIRECTORY_SERVICE_REQUIRED | DS_BACKGROUND_ONLY
int flags = unchecked((int)(0x00020000 | 0x80000000 | 0x00000010 | 0x00000100));
try
{
UnsafeNativeMethods.DomainControllerInfo info = Utils.GetDcName(null, dnsDomainName, null, flags);
this.domainFlatName = info.DomainName;
this.forestDnsName = info.DnsForestName;
}
catch (PrincipalOperationException ex) when (ex.ErrorCode == 1355) // ERROR_NO_SUCH_DOMAIN
{
// We couldn't get the NetBios name using DsGetDcName. This could be because we are running on
// a computer which is not connected to the forest of the domain at all. In such a case, we fall back
// to using the caller-specified DNS domain name. It may not work in 100% of cases, but it's better than
// simply failing outright, and if it's not good enough, we'll run into an appropriate error down the line
// when other calls fail.
GlobalDebug.WriteLineIf(GlobalDebug.Warn,
"ADStoreCtx",
"GetDcName failed, so falling back to the DNS domain name ({0}) for the Flat/DNS names",
dnsDomainName);
this.domainFlatName = dnsDomainName;
this.forestDnsName = dnsDomainName;
}
GlobalDebug.WriteLineIf(GlobalDebug.Info,
"ADStoreCtx",
"LoadComputerInfo: using domainFlatName={0}, forestDnsName={1}",
this.domainFlatName,
this.forestDnsName);
//
// Lockout duration
//
// Query the domain NC head to determine the lockout duration. Note that this is stored
// on the server as a negative filetime.
//
DirectoryEntry domainNC = SDSUtils.BuildDirectoryEntry(
"LDAP://" + this.dnsHostName + "/" + this.defaultNamingContext,
this.credentials,
this.authTypes);
// So we don't load every property
domainNC.RefreshCache(s_lockoutDuration);
if (domainNC.Properties["lockoutDuration"].Count > 0)
{
Debug.Assert(domainNC.Properties["lockoutDuration"].Count == 1);
long negativeLockoutDuration = ADUtils.LargeIntToInt64((UnsafeNativeMethods.IADsLargeInteger)domainNC.Properties["lockoutDuration"][0]);
Debug.Assert(negativeLockoutDuration <= 0);
ulong lockoutDuration = (ulong)(-negativeLockoutDuration);
this.lockoutDuration = lockoutDuration;
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "LoadComputerInfo: using lockout duration {0}", lockoutDuration);
}
//
// User supplied name
//
UnsafeNativeMethods.Pathname pathCracker = new UnsafeNativeMethods.Pathname();
UnsafeNativeMethods.IADsPathname pathName = (UnsafeNativeMethods.IADsPathname)pathCracker;
pathName.Set(this.ctxBase.Path, 1 /* ADS_SETTYPE_FULL */);
try
{
this.userSuppliedServerName = pathName.Retrieve(9 /*ADS_FORMAT_SERVER */);
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "LoadComputerInfo: using user-supplied name {0}", this.userSuppliedServerName);
}
catch (COMException e)
{
if (((uint)e.ErrorCode) == ((uint)0x80005000)) // E_ADS_BAD_PATHNAME
{
// Serverless path
GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "LoadComputerInfo: using empty string as user-supplied name");
this.userSuppliedServerName = "";
}
else
{
GlobalDebug.WriteLineIf(GlobalDebug.Error,
"ADStoreCtx",
"LoadComputerInfo: caught COMException {0} {1} looking for user-supplied name",
e.ErrorCode,
e.Message);
throw;
}
}
}
internal override bool IsValidProperty(Principal p, string propertyName)
{
return ((Hashtable)s_propertyMappingTableByProperty[this.MappingTableIndex]).Contains(propertyName);
}
}
}
|