PowerShell/src/Microsoft.PowerShell.LocalAccounts/LocalAccounts/Sam.cs
xtqqczze 883ca98dd7
Seal private classes (#15725)
* Seal private classes

* Fix CS0509

* Fix CS0628
2021-07-19 14:09:12 +05:00

3277 lines
127 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.PowerShell.Commands;
using System.Management.Automation.SecurityAccountsManager.Extensions;
using System.Management.Automation.SecurityAccountsManager.Native;
using System.Management.Automation.SecurityAccountsManager.Native.NtSam;
using System.Text;
using Microsoft.PowerShell.LocalAccounts;
using System.Diagnostics.CodeAnalysis;
[module: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")]
namespace System.Management.Automation.SecurityAccountsManager
{
/// <summary>
/// Defines enumeration constants for enabling and disabling something.
/// </summary>
internal enum Enabling
{
Disable = 0,
Enable
}
/// <summary>
/// Managed version of the SAM_RID_ENUMERATION native structure,
/// to be returned from the EnumerateLocalUsers method of Sam.
/// Contains the original structure's members along with additional
/// members of use.
/// </summary>
internal class SamRidEnumeration
{
#region Original struct members
public string Name;
public UInt32 RelativeId;
#endregion Original struct members
#region Additional members
public IntPtr domainHandle; // The domain handle used to acquire the data.
#endregion Additional members
}
/// <summary>
/// Provides methods for manipulating local Users and Groups.
/// </summary>
internal class Sam : IDisposable
{
#region Enums
[Flags]
private enum GroupProperties
{
Name = 0x0001, // NOT changeable through Set-LocalGroup
Description = 0x0002,
AllSetable = Description,
AllReadable = AllSetable | Name
}
/// <summary>
/// Defines a set of flags, each corresponding to a member of LocalUser,
/// which indicate fields to be updated.
/// </summary>
/// <remarks>
/// Although password can be set through Create-LocalUser and Set-LocalUser,
/// it is not a member of LocalUser so does not appear in this enumeration.
/// </remarks>
[Flags]
private enum UserProperties
{
None = 0x0000, // not actually a LocalUser member
Name = 0x0001, // NOT changeable through Set-LocalUser
AccountExpires = 0x0002,
Description = 0x0004,
Enabled = 0x0008, // NOT changeable through Set-LocalUser
FullName = 0x0010,
PasswordChangeableDate = 0x0020,
PasswordExpires = 0x0040,
PasswordNeverExpires = 0x0080,
UserMayChangePassword = 0x0100,
PasswordRequired = 0x0200,
PasswordLastSet = 0x0400, // CANNOT be set by cmdlet
LastLogon = 0x0800, // CANNOT be set by cmdlet
// All properties that can be set through Set-LocalUser
AllSetable = AccountExpires
| Description
| FullName
| PasswordChangeableDate
| PasswordExpires
| PasswordNeverExpires
| UserMayChangePassword
| PasswordRequired,
// Properties that can be set by Create-LocalUser
AllCreateable = AllSetable | Name | Enabled,
// Properties that can be read by e.g., Get-LocalUser
AllReadable = AllCreateable | PasswordLastSet | LastLogon
}
private enum PasswordExpiredState
{
Unchanged = -1,
NotExpired = 0,
Expired = 1
}
[Flags]
internal enum ObjectAccess : uint
{
AliasRead = Win32.STANDARD_RIGHTS_READ
| ALIAS_LIST_MEMBERS,
ALiasWrite = Win32.STANDARD_RIGHTS_WRITE
| ALIAS_WRITE_ACCOUNT
| ALIAS_ADD_MEMBER
| ALIAS_REMOVE_MEMBER,
UserAllAccess = Win32.STANDARD_RIGHTS_REQUIRED
| USER_READ_PREFERENCES
| USER_READ_LOGON
| USER_LIST_GROUPS
| USER_READ_GROUP_INFORMATION
| USER_WRITE_PREFERENCES
| USER_CHANGE_PASSWORD
| USER_FORCE_PASSWORD_CHANGE
| USER_READ_GENERAL
| USER_READ_ACCOUNT
| USER_WRITE_ACCOUNT
| USER_WRITE_GROUP_INFORMATION,
UserRead = Win32.STANDARD_RIGHTS_READ
| USER_READ_GENERAL // not in original USER_READ
| USER_READ_PREFERENCES
| USER_READ_LOGON
| USER_READ_ACCOUNT
| USER_LIST_GROUPS
| USER_READ_GROUP_INFORMATION,
UserWrite = Win32.STANDARD_RIGHTS_WRITE
| USER_WRITE_PREFERENCES
| USER_CHANGE_PASSWORD
}
[Flags]
internal enum DomainAccess : uint
{
AllAccess = Win32.STANDARD_RIGHTS_REQUIRED
| DOMAIN_READ_OTHER_PARAMETERS
| DOMAIN_WRITE_OTHER_PARAMETERS
| DOMAIN_WRITE_PASSWORD_PARAMS
| DOMAIN_CREATE_USER
| DOMAIN_CREATE_GROUP
| DOMAIN_CREATE_ALIAS
| DOMAIN_GET_ALIAS_MEMBERSHIP
| DOMAIN_LIST_ACCOUNTS
| DOMAIN_READ_PASSWORD_PARAMETERS
| DOMAIN_LOOKUP
| DOMAIN_ADMINISTER_SERVER,
Read = Win32.STANDARD_RIGHTS_READ
| DOMAIN_LIST_ACCOUNTS
| DOMAIN_GET_ALIAS_MEMBERSHIP
| DOMAIN_READ_OTHER_PARAMETERS,
Write = Win32.STANDARD_RIGHTS_WRITE
| DOMAIN_WRITE_OTHER_PARAMETERS
| DOMAIN_WRITE_PASSWORD_PARAMS
| DOMAIN_CREATE_USER
| DOMAIN_CREATE_GROUP
| DOMAIN_CREATE_ALIAS
| DOMAIN_ADMINISTER_SERVER,
Max = Win32.MAXIMUM_ALLOWED
}
/// <summary>
/// The operation under way. Used in the <see cref="Context"/> class.
/// </summary>
private enum ContextOperation
{
New = 1,
Enable,
Disable,
Get,
Remove,
Rename,
Set,
AddMember,
GetMember,
RemoveMember
}
/// <summary>
/// The type of object currently operating with.
/// used in the <see cref="Context"/> class.
/// </summary>
private enum ContextObjectType
{
User = 1,
Group
}
#endregion Enums
#region Internal Classes
/// <summary>
/// Holds information about the underway operation.
/// </summary>
/// <remarks>
/// Used primarily by the private ThrowOnFailure method when building
/// Exception objects to throw.
/// </remarks>
private sealed class Context
{
public ContextOperation operation;
public ContextObjectType type;
public object target;
public string objectId;
public string memberId;
/// <summary>
/// Initialize a new Context object.
/// </summary>
/// <param name="operation">
/// One of the <see cref="ContextOperation"/> enumerations indicating
/// the type of operation under way.
/// </param>
/// <param name="objectType">
/// One of the <see cref="ContextObjectType"/> enumerations indicating
/// the type of object (user or group) being used.
/// </param>
/// <param name="objectIdentifier">
/// A string containing the name of the object. This may be either a
/// user/group name or a string representation of a SID.
/// </param>
/// <param name="target">
/// The target being operated on.
/// </param>
/// <param name="memberIdentifier">
/// A string containing the name of the member being added or removed
/// from a group. Used only in such cases.
/// </param>
public Context(ContextOperation operation,
ContextObjectType objectType,
string objectIdentifier,
object target,
string memberIdentifier = null)
{
this.operation = operation;
this.type = objectType;
this.objectId = objectIdentifier;
this.target = target;
this.memberId = memberIdentifier;
}
/// <summary>
/// Default constructor.
/// </summary>
public Context()
{
}
/// <summary>
/// Gets a string containing the type of operation under way.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
public string OperationName
{
get { return operation.ToString(); }
}
/// <summary>
/// Gets a string containing the type of object ("User" or "Group")
/// being used.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
public string TypeNamne
{
get { return type.ToString(); }
}
/// <summary>
/// Gets a string containing the name of the object being used.
/// </summary>
public string ObjectName
{
get { return objectId; }
}
/// <summary>
/// Gets a string containing the name of the member being added to
/// or removed from a group. Returns null if the operation does not
/// involve group members.
/// </summary>
public string MemberName
{
get { return memberId; }
}
}
/// <summary>
/// Contains basic information about an Account.
/// </summary>
/// <remarks>
/// AccountInfo is the return type from the private
/// LookupAccountInfo method.
/// </remarks>
private sealed class AccountInfo
{
public string AccountName;
public string DomainName;
public SecurityIdentifier Sid;
public Native.SID_NAME_USE Use;
public override string ToString()
{
if (!string.IsNullOrEmpty(DomainName))
return DomainName + '\\' + AccountName;
else
return AccountName;
}
}
#endregion Internal Classes
#region Constants
//
// Access rights
//
private const UInt32 ALIAS_ADD_MEMBER = 0x0001;
private const UInt32 ALIAS_REMOVE_MEMBER = 0x0002;
private const UInt32 ALIAS_LIST_MEMBERS = 0x0004;
private const UInt32 ALIAS_READ_INFORMATION = 0x0008;
private const UInt32 ALIAS_WRITE_ACCOUNT = 0x0010;
private const UInt32 USER_READ_GENERAL = 0x0001;
private const UInt32 USER_READ_PREFERENCES = 0x0002;
private const UInt32 USER_WRITE_PREFERENCES = 0x0004;
private const UInt32 USER_READ_LOGON = 0x0008;
private const UInt32 USER_READ_ACCOUNT = 0x0010;
private const UInt32 USER_WRITE_ACCOUNT = 0x0020;
private const UInt32 USER_CHANGE_PASSWORD = 0x0040;
private const UInt32 USER_FORCE_PASSWORD_CHANGE = 0x0080;
private const UInt32 USER_LIST_GROUPS = 0x0100;
private const UInt32 USER_READ_GROUP_INFORMATION = 0x0200;
private const UInt32 USER_WRITE_GROUP_INFORMATION = 0x0400;
private const UInt32 DOMAIN_READ_PASSWORD_PARAMETERS = 0x0001;
private const UInt32 DOMAIN_WRITE_PASSWORD_PARAMS = 0x0002;
private const UInt32 DOMAIN_READ_OTHER_PARAMETERS = 0x0004;
private const UInt32 DOMAIN_WRITE_OTHER_PARAMETERS = 0x0008;
private const UInt32 DOMAIN_CREATE_USER = 0x0010;
private const UInt32 DOMAIN_CREATE_GROUP = 0x0020;
private const UInt32 DOMAIN_CREATE_ALIAS = 0x0040;
private const UInt32 DOMAIN_GET_ALIAS_MEMBERSHIP = 0x0080;
private const UInt32 DOMAIN_LIST_ACCOUNTS = 0x0100;
private const UInt32 DOMAIN_LOOKUP = 0x0200;
private const UInt32 DOMAIN_ADMINISTER_SERVER = 0x0400;
#endregion Constants
#region Static Data
private static SecurityIdentifier worldSid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
#endregion Static Data
#region Instance Data
private IntPtr samHandle = IntPtr.Zero;
private IntPtr localDomainHandle = IntPtr.Zero;
private IntPtr builtinDomainHandle = IntPtr.Zero;
private Context context = null;
private string machineName = string.Empty;
#endregion Instance Data
#region Construction
internal Sam()
{
OpenHandles();
// CoreCLR does not have Environment.MachineName,
// so we'll use this instead.
machineName = System.Net.Dns.GetHostName();
}
#endregion Construction
#region Public (Internal) Methods
public string StripMachineName(string name)
{
var mn = machineName + '\\';
if (name.StartsWith(mn, StringComparison.CurrentCultureIgnoreCase))
return name.Substring(mn.Length);
return name;
}
#region Local Groups
/// <summary>
/// Retrieve a named local group.
/// </summary>
/// <param name="groupName">Name of the desired local group.</param>
/// <returns>
/// A <see cref="LocalGroup"/> object containing information about
/// the local group.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown when the named group cannot be found.
/// </exception>
internal LocalGroup GetLocalGroup(string groupName)
{
context = new Context(ContextOperation.Get, ContextObjectType.Group, groupName, groupName);
foreach (var sre in EnumerateGroups())
if (sre.Name.Equals(groupName, StringComparison.CurrentCultureIgnoreCase))
return MakeLocalGroupObject(sre); // return a populated group
throw new GroupNotFoundException(groupName, context.target);
}
/// <summary>
/// Retrieve a local group by SID.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the desired group.
/// </param>
/// <returns>
/// A <see cref="LocalGroup"/> object containing information about
/// the local group.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown when the specified group cannot be found.
/// </exception>
internal LocalGroup GetLocalGroup(SecurityIdentifier sid)
{
context = new Context(ContextOperation.Get, ContextObjectType.Group, sid.ToString(), sid);
foreach (var sre in EnumerateGroups())
if (RidToSid(sre.domainHandle, sre.RelativeId) == sid)
return MakeLocalGroupObject(sre); // return a populated group
throw new GroupNotFoundException(sid.ToString(), context.target);
}
/// <summary>
/// Create a local group.
/// </summary>
/// <param name="group">A <see cref="LocalGroup"/> object containing
/// information about the local group to be created.
/// </param>
/// <returns>
/// A new LocalGroup object containing information about the newly
/// created local group.
/// </returns>
/// <exception cref="GroupExistsException">
/// Thrown when an attempt is made to create a local group that already
/// exists.
/// </exception>
internal LocalGroup CreateLocalGroup(LocalGroup group)
{
context = new Context(ContextOperation.New, ContextObjectType.Group, group.Name, group.Name);
return CreateGroup(group, localDomainHandle);
}
/// <summary>
/// Update a local group with new property values.
/// </summary>
/// <param name="group">
/// A <see cref="LocalGroup"/> object representing the group to be updated.
/// </param>
/// <param name="changed">
/// A LocalGroup object containing the desired changes.
/// </param>
/// <remarks>
/// Currently, a group's description is the only changeable property.
/// </remarks>
internal void UpdateLocalGroup(LocalGroup group, LocalGroup changed)
{
context = new Context(ContextOperation.Set, ContextObjectType.Group, group.Name, group);
UpdateGroup(group, changed);
}
/// <summary>
/// Remove a local group.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the
/// local group to be removed.
/// </param>
/// <exception cref="GroupNotFoundException">
/// Thrown when the specified group cannot be found.
/// </exception>
internal void RemoveLocalGroup(SecurityIdentifier sid)
{
context = new Context(ContextOperation.Remove, ContextObjectType.Group, sid.ToString(), sid);
RemoveGroup(sid);
}
/// <summary>
/// Remove a local group.
/// </summary>
/// <param name="group">
/// A <see cref="LocalGroup"/> object containing
/// information about the local group to be removed.
/// </param>
/// <exception cref="GroupNotFoundException">
/// Thrown when the specified group cannot be found.
/// </exception>
internal void RemoveLocalGroup(LocalGroup group)
{
context = new Context(ContextOperation.Remove, ContextObjectType.Group, group.Name, group);
if (group.SID == null)
context.target = group = GetLocalGroup(group.Name);
RemoveGroup(group.SID);
}
/// <summary>
/// Rename a local group.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying
/// the local group to be renamed.
/// </param>
/// <param name="newName">
/// A string containing the new name for the local group.
/// </param>
/// <exception cref="GroupNotFoundException">
/// Thrown when the specified group cannot be found.
/// </exception>
internal void RenameLocalGroup(SecurityIdentifier sid, string newName)
{
context = new Context(ContextOperation.Rename, ContextObjectType.Group, sid.ToString(), sid);
RenameGroup(sid, newName);
}
/// <summary>
/// Rename a local group.
/// </summary>
/// <param name="group">
/// A <see cref="LocalGroup"/> object containing
/// information about the local group to be renamed.
/// </param>
/// <param name="newName">
/// A string containing the new name for the local group.
/// </param>
/// <exception cref="GroupNotFoundException">
/// Thrown when the specified group cannot be found.
/// </exception>
internal void RenameLocalGroup(LocalGroup group, string newName)
{
context = new Context(ContextOperation.Rename, ContextObjectType.Group, group.Name, group);
if (group.SID == null)
context.target = group = GetLocalGroup(group.Name);
RenameGroup(group.SID, newName);
}
/// <summary>
/// Get all local groups whose names satisfy the specified predicate.
/// </summary>
/// <param name="pred">
/// Predicate that determines whether a group satisfies the conditions.
/// </param>
/// <returns>
/// An <see cref="IEnumerable{LocalGroup}"/> object containing LocalGroup
/// objects that satisfy the predicate condition.
/// </returns>
internal IEnumerable<LocalGroup> GetMatchingLocalGroups(Predicate<string> pred)
{
context = new Context(ContextOperation.Get, ContextObjectType.Group, string.Empty, null);
foreach (var sre in EnumerateGroups())
{
if (pred(sre.Name))
{
context.target = sre.Name;
yield return MakeLocalGroupObject(sre);
}
}
}
/// <summary>
/// Get all local groups.
/// </summary>
/// <returns>
/// An <see cref="IEnumerable{LocalGroup}"/> object containing a
/// LocalGroup object for each local group.
/// </returns>
internal IEnumerable<LocalGroup> GetAllLocalGroups()
{
context = new Context(ContextOperation.Get, ContextObjectType.Group, string.Empty, null);
foreach (var sre in EnumerateGroups())
{
context.target = sre.Name;
yield return MakeLocalGroupObject(sre);
}
}
/// <summary>
/// Add members to a local group.
/// </summary>
/// <param name="group">
/// A <see cref="LocalGroup"/> object identifying the group to
/// which to add members.
/// </param>
/// <param name="member">
/// An object of type <see cref="LocalPrincipal"/> identifying
/// the member to be added.
/// </param>
/// <returns>
/// An Exception object indicating any errors encountered.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown if the group could not be found.
/// </exception>
internal Exception AddLocalGroupMember(LocalGroup group, LocalPrincipal member)
{
context = new Context(ContextOperation.AddMember, ContextObjectType.Group, group.Name, group);
if (group.SID == null)
context.target = group = GetLocalGroup(group.Name);
return AddGroupMember(group.SID, member);
}
/// <summary>
/// Add members to a local group.
/// </summary>
/// <param name="groupSid">
/// A <see cref="SecurityIdentifier"/> object identifying the group to
/// which to add members.
/// </param>
/// <param name="member">
/// An object of type <see cref="LocalPrincipal"/> identifying
/// the member to be added.
/// </param>
/// <returns>
/// An Exception object indicating any errors encountered.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown if the group could not be found.
/// </exception>
internal Exception AddLocalGroupMember(SecurityIdentifier groupSid, LocalPrincipal member)
{
context = new Context(ContextOperation.AddMember, ContextObjectType.Group, groupSid.ToString(), groupSid);
return AddGroupMember(groupSid, member);
}
/// <summary>
/// Retrieve members of a Local group.
/// </summary>
/// <param name="group">
/// A <see cref="LocalGroup"/> object identifying the group whose members
/// are requested.
/// </param>
/// <returns>
/// An IEnumerable of <see cref="LocalPrincipal"/> objects containing the group's
/// members.
/// </returns>
internal IEnumerable<LocalPrincipal> GetLocalGroupMembers(LocalGroup group)
{
context = new Context(ContextOperation.GetMember, ContextObjectType.Group, group.Name, group);
if (group.SID == null)
context.target = group = GetLocalGroup(group.Name);
return GetGroupMembers(group.SID);
}
/// <summary>
/// Retrieve members of a Local group.
/// </summary>
/// <param name="groupSid">
/// A <see cref="SecurityIdentifier"/> object identifying the group whose members
/// are requested.
/// </param>
/// <returns>
/// An IEnumerable of <see cref="LocalPrincipal"/> objects containing the group's
/// members.
/// </returns>
internal IEnumerable<LocalPrincipal> GetLocalGroupMembers(SecurityIdentifier groupSid)
{
context = new Context(ContextOperation.GetMember, ContextObjectType.Group, groupSid.ToString(), groupSid);
return GetGroupMembers(groupSid);
}
/// <summary>
/// Remove members from a local group.
/// </summary>
/// <param name="group">
/// A <see cref="LocalGroup"/> object identifying the group from
/// which to remove members
/// </param>
/// <param name="member">
/// An object of type <see cref="LocalPrincipal"/> identifying
/// the member to be removed.
/// </param>
/// <returns>
/// An Exception object indicating any errors encountered.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown if the group could not be found.
/// </exception>
internal Exception RemoveLocalGroupMember(LocalGroup group, LocalPrincipal member)
{
context = new Context(ContextOperation.RemoveMember, ContextObjectType.Group, group.Name, group);
if (group.SID == null)
context.target = group = GetLocalGroup(group.Name);
return RemoveGroupMember(group.SID, member);
}
/// <summary>
/// Remove members from a local group.
/// </summary>
/// <param name="groupSid">
/// A <see cref="SecurityIdentifier"/> object identifying the group from
/// which to remove members
/// </param>
/// <param name="member">
/// An Object of type <see cref="LocalPrincipal"/> identifying
/// the member to be removed.
/// </param>
/// <returns>
/// An Exception object indicating any errors encountered.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown if the group could not be found.
/// </exception>
internal Exception RemoveLocalGroupMember(SecurityIdentifier groupSid, LocalPrincipal member)
{
context = new Context(ContextOperation.RemoveMember, ContextObjectType.Group, groupSid.ToString(), groupSid);
return RemoveGroupMember(groupSid, member);
}
#endregion Local Groups
#region Local Users
/// <summary>
/// Retrieve a named local user.
/// </summary>
/// <param name="userName">Name of the desired local user.</param>
/// <returns>
/// A <see cref="LocalUser"/> object containing information about
/// the local user.
/// </returns>
/// <exception cref="UserNotFoundException">
/// Thrown when the named user cannot be found.
/// </exception>
internal LocalUser GetLocalUser(string userName)
{
context = new Context(ContextOperation.Get, ContextObjectType.User, userName, userName);
foreach (var sre in EnumerateUsers())
if (sre.Name.Equals(userName, StringComparison.CurrentCultureIgnoreCase))
return MakeLocalUserObject(sre);
throw new UserNotFoundException(userName, userName);
}
/// <summary>
/// Retrieve a local user by SID.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the desired user.
/// </param>
/// <returns>
/// A <see cref="LocalUser"/> object containing information about
/// the local user.
/// </returns>
/// <exception cref="UserNotFoundException">
/// Thrown when the specified user cannot be found.
/// </exception>
internal LocalUser GetLocalUser(SecurityIdentifier sid)
{
context = new Context(ContextOperation.Get, ContextObjectType.User, sid.ToString(), sid);
foreach (var sre in EnumerateUsers())
if (RidToSid(sre.domainHandle, sre.RelativeId) == sid)
return MakeLocalUserObject(sre); // return a populated user
throw new UserNotFoundException(sid.ToString(), sid);
}
/// <summary>
/// Create a local user.
/// </summary>
/// <param name="user">A <see cref="LocalUser"/> object containing
/// information about the local user to be created.
/// </param>
/// <param name="password">A <see cref="System.Security.SecureString"/> containing
/// the initial password to be set for the new local user. If this parameter is null,
/// no password is set.
/// </param>
/// <param name="setPasswordNeverExpires">
/// Indicates whether PasswordNeverExpires was specified
/// </param>
/// <returns>
/// A new LocalGroup object containing information about the newly
/// created local user.
/// </returns>
/// <exception cref="UserExistsException">
/// Thrown when an attempt is made to create a local user that already
/// exists.
/// </exception>
internal LocalUser CreateLocalUser(LocalUser user, System.Security.SecureString password, bool setPasswordNeverExpires)
{
context = new Context(ContextOperation.New, ContextObjectType.User, user.Name, user);
return CreateUser(user, password, localDomainHandle, setPasswordNeverExpires);
}
/// <summary>
/// Remove a local user.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying
/// the local user to be removed.
/// </param>
/// <exception cref="UserNotFoundException">
/// Thrown when the specified user cannot be found.
/// </exception>
internal void RemoveLocalUser(SecurityIdentifier sid)
{
context = new Context(ContextOperation.Remove, ContextObjectType.User, sid.ToString(), sid);
RemoveUser(sid);
}
/// <summary>
/// Remove a local user.
/// </summary>
/// <param name="user">
/// A <see cref="LocalUser"/> object containing
/// information about the local user to be removed.
/// </param>
/// <exception cref="UserNotFoundException">
/// Thrown when the specified user cannot be found.
/// </exception>
internal void RemoveLocalUser(LocalUser user)
{
context = new Context(ContextOperation.Remove, ContextObjectType.User, user.Name, user);
if (user.SID == null)
context.target = user = GetLocalUser(user.Name);
RemoveUser(user.SID);
}
/// <summary>
/// Rename a local user.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> objects identifying
/// the local user to be renamed.
/// </param>
/// <param name="newName">
/// A string containing the new name for the local user.
/// </param>
/// <exception cref="UserNotFoundException">
/// Thrown when the specified user cannot be found.
/// </exception>
internal void RenameLocalUser(SecurityIdentifier sid, string newName)
{
context = new Context(ContextOperation.Rename, ContextObjectType.User, sid.ToString(), sid);
RenameUser(sid, newName);
}
/// <summary>
/// Rename a local user.
/// </summary>
/// <param name="user">
/// A <see cref="LocalUser"/> objects containing
/// information about the local user to be renamed.
/// </param>
/// <param name="newName">
/// A string containing the new name for the local user.
/// </param>
/// <exception cref="UserNotFoundException">
/// Thrown when the specified user cannot be found.
/// </exception>
internal void RenameLocalUser(LocalUser user, string newName)
{
context = new Context(ContextOperation.Rename, ContextObjectType.User, user.Name, user);
if (user.SID == null)
context.target = user = GetLocalUser(user.Name);
RenameUser(user.SID, newName);
}
/// <summary>
/// Enable or disable a Local User.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the user to enable or disable.
/// </param>
/// <param name="enable">
/// One of the <see cref="Enabling"/> enumeration values, indicating whether to
/// enable or disable the user.
/// </param>
internal void EnableLocalUser(SecurityIdentifier sid, Enabling enable)
{
context = new Context(enable == Enabling.Enable ? ContextOperation.Enable
: ContextOperation.Disable,
ContextObjectType.User, sid.ToString(),
sid);
EnableUser(sid, enable);
}
/// <summary>
/// Enable or disable a Local User.
/// </summary>
/// <param name="user">
/// A <see cref="LocalUser"/> object representing the user to enable or disable.
/// </param>
/// <param name="enable">
/// One of the <see cref="Enabling"/> enumeration values, indicating whether to
/// enable or disable the user.
/// </param>
internal void EnableLocalUser(LocalUser user, Enabling enable)
{
context = new Context(enable == Enabling.Enable ? ContextOperation.Enable
: ContextOperation.Disable,
ContextObjectType.User, user.Name,
user);
if (user.SID == null)
context.target = user = GetLocalUser(user.Name);
EnableUser(user.SID, enable);
}
/// <summary>
/// Update a local user with new properties.
/// </summary>
/// <param name="user">
/// A <see cref="LocalUser"/> object representing the user to be updated.
/// </param>
/// <param name="changed">
/// A LocalUser object containing the desired changes.
/// </param>
/// <param name="password">A <see cref="System.Security.SecureString"/>
/// object containing the new password. A null value in this parameter
/// indicates that the password is not to be changed.
/// </param>
/// <param name="setPasswordNeverExpires">
/// Specifies whether the PasswordNeverExpires parameter was set.
/// </param>
/// <remarks>
/// Call this overload when intending to leave the password-expired
/// marker in its current state. To set the password and the
/// password-expired state, call the overload with a boolean as the
/// fourth parameter
/// </remarks>
internal void UpdateLocalUser(LocalUser user, LocalUser changed, System.Security.SecureString password, bool? setPasswordNeverExpires)
{
context = new Context(ContextOperation.Set, ContextObjectType.User, user.Name, user);
UpdateUser(user, changed, password, PasswordExpiredState.Unchanged, setPasswordNeverExpires);
}
/// <summary>
/// Get all local users whose names satisfy the specified predicate.
/// </summary>
/// <param name="pred">
/// Predicate that determines whether a user satisfies the conditions.
/// </param>
/// <returns>
/// An <see cref="IEnumerable{LocalUser}"/> object containing LocalUser
/// objects that satisfy the predicate condition.
/// </returns>
internal IEnumerable<LocalUser> GetMatchingLocalUsers(Predicate<string> pred)
{
context = new Context(ContextOperation.Get, ContextObjectType.User, string.Empty, null);
foreach (var sre in EnumerateUsers())
{
if (pred(sre.Name))
{
context.target = sre.Name;
yield return MakeLocalUserObject(sre);
}
}
}
/// <summary>
/// Get all local users.
/// </summary>
/// <returns>
/// An <see cref="IEnumerable{LocalUser}"/> object containing a
/// LocalUser object for each local user.
/// </returns>
internal IEnumerable<LocalUser> GetAllLocalUsers()
{
context = new Context(ContextOperation.Get, ContextObjectType.User, null, null);
foreach (var sre in EnumerateUsers())
yield return MakeLocalUserObject(sre);
}
#endregion Local Users
#region Local Principals
internal LocalPrincipal LookupAccount(string name)
{
var info = LookupAccountInfo(name);
if (info == null)
throw new PrincipalNotFoundException(name, name);
return MakeLocalPrincipalObject(info);
}
#endregion Local Principals
#endregion Public (Internal) Methods
#region Private Methods
/// <summary>
/// Open the handles stored by Sam instances.
/// </summary>
private void OpenHandles()
{
var systemName = new UNICODE_STRING();
var oa = new OBJECT_ATTRIBUTES();
IntPtr pInfo = IntPtr.Zero;
IntPtr pSid = IntPtr.Zero;
IntPtr lsaHandle = IntPtr.Zero;
UInt32 status = 0;
try
{
status = Win32.LsaOpenPolicy(ref systemName, ref oa, (UInt32)LSA_AccessPolicy.POLICY_VIEW_LOCAL_INFORMATION, out lsaHandle);
ThrowOnFailure(status);
POLICY_PRIMARY_DOMAIN_INFO domainInfo;
status = Win32.LsaQueryInformationPolicy(lsaHandle,
POLICY_INFORMATION_CLASS.PolicyAccountDomainInformation,
out pInfo);
ThrowOnFailure(status);
status = Win32.LsaClose(lsaHandle);
ThrowOnFailure(status);
lsaHandle = IntPtr.Zero;
domainInfo = Marshal.PtrToStructure<POLICY_PRIMARY_DOMAIN_INFO>(pInfo);
status = SamApi.SamConnect(ref systemName, out samHandle, SamApi.SAM_SERVER_LOOKUP_DOMAIN, ref oa);
ThrowOnFailure(status);
// Open the local domain
status = SamApi.SamOpenDomain(samHandle, Win32.MAXIMUM_ALLOWED, domainInfo.Sid, out localDomainHandle);
ThrowOnFailure(status);
// Open the "BuiltIn" domain
SecurityIdentifier sid = new SecurityIdentifier("S-1-5-32");
byte[] bSid = new byte[sid.BinaryLength];
int size = Marshal.SizeOf<byte>() * bSid.Length;
pSid = Marshal.AllocHGlobal(size);
sid.GetBinaryForm(bSid, 0);
Marshal.Copy(bSid, 0, pSid, bSid.Length);
status = SamApi.SamOpenDomain(samHandle, Win32.MAXIMUM_ALLOWED, pSid, out builtinDomainHandle);
ThrowOnFailure(status);
}
finally
{
if (pInfo != IntPtr.Zero)
status = Win32.LsaFreeMemory(pInfo);
Marshal.FreeHGlobal(pSid);
if (lsaHandle != IntPtr.Zero)
status = Win32.LsaClose(lsaHandle);
}
}
/// <summary>
/// Find a group by SID and return a <see cref="SamRidEnumeration"/> object
/// representing the group.
/// </summary>
/// <param name="sid">A <see cref="SecurityIdentifier"/> object identifying
/// the group to search for.</param>
/// <returns>
/// A SamRidEnumeration object representing the group.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown when the specified group is not found.
/// </exception>
/// <remarks>
/// This method saves some time and effort over the GetGroup method
/// because it does not have to open a group to populate a full Group
/// object.
/// </remarks>
private SamRidEnumeration GetGroupSre(SecurityIdentifier sid)
{
foreach (var sre in EnumerateGroups())
if (RidToSid(sre.domainHandle, sre.RelativeId) == sid)
return sre;
throw new GroupNotFoundException(sid.ToString(), sid);
}
/// <summary>
/// Find a user by SID and return a <see cref="SamRidEnumeration"/> object
/// representing the user.
/// </summary>
/// <param name="sid">A <see cref="SecurityIdentifier"/> object identifying
/// the user to search for.</param>
/// <returns>
/// A SamRidEnumeration object representing the user.
/// </returns>
/// <exception cref="UserNotFoundException">
/// Thrown when the specified user is not found.
/// </exception>
/// <remarks>
/// This method saves some time and effort over the GetUser method
/// because it does not have to open a user to populate a full LocalUser
/// object.
/// </remarks>
private SamRidEnumeration GetUserSre(SecurityIdentifier sid)
{
foreach (var sre in EnumerateUsers())
if (RidToSid(sre.domainHandle, sre.RelativeId) == sid)
return sre;
throw new UserNotFoundException(sid.ToString(), sid);
}
/// <summary>
/// Enumerate local users with native SAM functions.
/// </summary>
/// <param name="domainHandle">Handle to the domain to enumerate over.</param>
/// <returns>
/// An IEnumerable of SamRidEnumeration objects, one for each local user.
/// </returns>
/// <remarks>
/// This is a "generator" method. Rather than returning an entire collection,
/// it uses 'yield return' to return each object in turn.
/// </remarks>
private static IEnumerable<SamRidEnumeration> EnumerateUsersInDomain(IntPtr domainHandle)
{
UInt32 status = 0;
UInt32 context = 0;
IntPtr buffer = IntPtr.Zero;
UInt32 countReturned;
do
{
status = SamApi.SamEnumerateUsersInDomain(domainHandle,
ref context,
0,
out buffer,
1,
out countReturned);
if (status == NtStatus.STATUS_MORE_ENTRIES && countReturned == 1)
{
if (buffer != IntPtr.Zero)
{
SAM_RID_ENUMERATION sre;
sre = Marshal.PtrToStructure<SAM_RID_ENUMERATION>(buffer);
SamApi.SamFreeMemory(buffer);
buffer = IntPtr.Zero;
yield return new SamRidEnumeration
{
Name = sre.Name.ToString(),
RelativeId = sre.RelativeId,
domainHandle = domainHandle
};
}
}
} while (Succeeded(status) && status != 0 && countReturned != 0);
}
/// <summary>
/// Enumerate user objects in both the local and builtin domains.
/// </summary>
/// <returns>
/// An IEnumerable of SamRidEnumeration objects, one for each local user.
/// </returns>
/// <remarks>
/// This is a "generator" method. Rather than returning an entire collection,
/// it uses 'yield return' to return each object in turn.
/// </remarks>
private IEnumerable<SamRidEnumeration> EnumerateUsers()
{
foreach (var sre in EnumerateUsersInDomain(localDomainHandle))
yield return sre;
foreach (var sre in EnumerateUsersInDomain(builtinDomainHandle))
yield return sre;
}
/// <summary>
/// Create a new user in the specified domain.
/// </summary>
/// <param name="userInfo">
/// A <see cref="LocalUser"/> object containing information about the new user.
/// </param>
/// <param name="password">A <see cref="System.Security.SecureString"/> containing
/// the initial password to be set for the new local user. If this parameter is null,
/// no password is set.
/// </param>
/// <param name="domainHandle">
/// Handle to the domain in which to create the new user.
/// </param>
/// <param name="setPasswordNeverExpires">
/// Indicates whether PasswordNeverExpires was specified
/// </param>
/// <returns>
/// A LocalUser object that represents the newly-created user
/// </returns>
private LocalUser CreateUser(LocalUser userInfo, System.Security.SecureString password, IntPtr domainHandle, bool setPasswordNeverExpires)
{
IntPtr userHandle = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero;
UNICODE_STRING str = new UNICODE_STRING();
UInt32 status = 0;
try
{
UInt32 relativeId = 0;
UInt32 grantedAccess = 0;
str = new UNICODE_STRING(userInfo.Name);
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(str));
Marshal.StructureToPtr(str, buffer, false);
status = SamApi.SamCreateUser2InDomain(domainHandle,
ref str,
(int) SamApi.USER_NORMAL_ACCOUNT,
Win32.MAXIMUM_ALLOWED,
out userHandle,
out grantedAccess,
out relativeId);
Marshal.DestroyStructure<UNICODE_STRING>(buffer);
Marshal.FreeHGlobal(buffer);
buffer = IntPtr.Zero;
ThrowOnFailure(status);
// set the various properties of the user. A SID is required because some
// operations depend on it.
userInfo.SID = RidToSid(domainHandle, relativeId);
SetUserData(userHandle, userInfo, UserProperties.AllCreateable, password, PasswordExpiredState.NotExpired, setPasswordNeverExpires);
return MakeLocalUserObject(new SamRidEnumeration
{
domainHandle = domainHandle,
Name = userInfo.Name,
RelativeId = relativeId
},
userHandle);
}
catch (Exception)
{
if (IntPtr.Zero != userHandle)
{
SamApi.SamDeleteUser(userHandle);
}
throw;
}
finally
{
if (buffer != IntPtr.Zero)
Marshal.FreeHGlobal(buffer);
if (userHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(userHandle);
}
}
/// <summary>
/// Remove a group identified by SID.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the
/// group to be removed.
/// </param>
private void RemoveGroup(SecurityIdentifier sid)
{
var sre = GetGroupSre(sid);
IntPtr aliasHandle = IntPtr.Zero;
UInt32 status;
try
{
status = SamApi.SamOpenAlias(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out aliasHandle);
ThrowOnFailure(status);
status = SamApi.SamDeleteAlias(aliasHandle);
ThrowOnFailure(status);
aliasHandle = IntPtr.Zero; // The handle is freed internally if SamDeleteAlias succeeds
}
finally
{
if (aliasHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(aliasHandle);
}
}
/// <summary>
/// Rename a group identified by SID.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying
/// the local group to be renamed.
/// </param>
/// <param name="newName">
/// A string containing the new name for the group.
/// </param>
/// <exception cref="GroupNotFoundException">
/// Thrown when the specified group cannot be found.
/// </exception>
private void RenameGroup(SecurityIdentifier sid, string newName)
{
var sre = GetGroupSre(sid);
IntPtr aliasHandle = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero;
UInt32 status = 0;
status = SamApi.SamOpenAlias(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out aliasHandle);
ThrowOnFailure(status);
try
{
ALIAS_NAME_INFORMATION info = new ALIAS_NAME_INFORMATION();
info.Name = new UNICODE_STRING(newName);
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(info));
Marshal.StructureToPtr(info, buffer, false);
status = SamApi.SamSetInformationAlias(aliasHandle,
ALIAS_INFORMATION_CLASS.AliasNameInformation,
buffer);
ThrowOnFailure(status,
new Context {
objectId = newName,
operation = context.operation,
type = context.type
}
);
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.DestroyStructure<ALIAS_NAME_INFORMATION>(buffer);
Marshal.FreeHGlobal(buffer);
}
if (aliasHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(aliasHandle);
}
}
/// <summary>
/// Add members to a group.
/// </summary>
/// <param name="groupSid">
/// A <see cref="SecurityIdentifier"/> object identifying the group to
/// which to add members.
/// </param>
/// <param name="member">
/// An object of type <see cref="LocalPrincipal"/>identifying
/// the member to be added.
/// </param>
/// <returns>
/// An Exception object indicating any errors encountered.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown if the group could not be found.
/// </exception>
private Exception AddGroupMember(SecurityIdentifier groupSid, LocalPrincipal member)
{
var sre = GetGroupSre(groupSid); // We'll let this throw if necessary
IntPtr aliasHandle = IntPtr.Zero;
UInt32 status = SamApi.SamOpenAlias(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out aliasHandle);
ThrowOnFailure(status);
Exception ex = null;
try
{
var sid = member.SID;
var binarySid = new byte[sid.BinaryLength];
sid.GetBinaryForm(binarySid, 0);
status = SamApi.SamAddMemberToAlias(aliasHandle, binarySid);
ex = MakeException(status,
new Context
{
memberId = member.ToString(),
objectId = context.objectId,
operation = context.operation,
target = context.target,
type = context.type
}
);
}
finally
{
if (aliasHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(aliasHandle);
}
return ex;
}
/// <summary>
/// Retrieve members of a group.
/// </summary>
/// <param name="groupSid">
/// A <see cref="SecurityIdentifier"/> object representing the group whose members
/// are requested.
/// </param>
/// <returns>
/// An IEnumerable of <see cref="LocalPrincipal"/> objects containing the group's
/// members.
/// </returns>
private IEnumerable<LocalPrincipal> GetGroupMembers(SecurityIdentifier groupSid)
{
var sre = GetGroupSre(groupSid);
IntPtr aliasHandle = IntPtr.Zero;
IntPtr memberIds = IntPtr.Zero;
UInt32 status = SamApi.SamOpenAlias(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out aliasHandle);
ThrowOnFailure(status);
try
{
UInt32 memberCount = 0;
status = SamApi.SamGetMembersInAlias(aliasHandle, out memberIds, out memberCount);
ThrowOnFailure(status);
if (memberCount != 0)
{
IntPtr[] idArray = new IntPtr[memberCount];
Marshal.Copy(memberIds, idArray, 0, (int)memberCount);
for (int i=0; i < memberCount; i++)
{
var sid = new SecurityIdentifier(idArray[i]);
yield return MakeLocalPrincipalObject(LookupAccountInfo(sid));
}
}
}
finally
{
if (aliasHandle != IntPtr.Zero)
SamApi.SamCloseHandle(aliasHandle);
if (memberIds != IntPtr.Zero)
SamApi.SamFreeMemory(memberIds);
}
}
/// <summary>
/// Remove members from a group.
/// </summary>
/// <param name="groupSid">
/// A <see cref="SecurityIdentifier"/> object identifying the group from
/// which to remove members
/// </param>
/// <param name="member">
/// An object of type <see cref="LocalPrincipal"/> identifying
/// the member to be removed.
/// </param>
/// <returns>
/// An IEnumerable of Exception objects indicating any errors encountered.
/// </returns>
/// <exception cref="GroupNotFoundException">
/// Thrown if the group could not be found.
/// </exception>
private Exception RemoveGroupMember(SecurityIdentifier groupSid, LocalPrincipal member)
{
var sre = GetGroupSre(groupSid); // We'll let this throw if necessary
IntPtr aliasHandle = IntPtr.Zero;
UInt32 status = SamApi.SamOpenAlias(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out aliasHandle);
ThrowOnFailure(status);
// Now we're processing each member, so any further exceptions will
// be stored in the collection and returned later.
var rv = new List<Exception>();
Exception ex = null;
try
{
var sid = member.SID;
var binarySid = new byte[sid.BinaryLength];
sid.GetBinaryForm(binarySid, 0);
status = SamApi.SamRemoveMemberFromAlias(aliasHandle, binarySid);
ex = MakeException(status,
new Context {
memberId = member.ToString(),
objectId = context.objectId,
operation = context.operation,
target = context.target,
type = context.type
}
);
}
finally
{
if (aliasHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(aliasHandle);
}
return ex;
}
/// <summary>
/// Create a populated LocalUser object from a SamRidEnumeration object.
/// </summary>
/// <param name="sre">
/// A <see cref="SamRidEnumeration"/> object containing minimal information
/// about a local user.
/// </param>
/// <returns>
/// A LocalUser object, populated with user information.
/// </returns>
private LocalUser MakeLocalUserObject(SamRidEnumeration sre)
{
IntPtr userHandle = IntPtr.Zero;
var status = SamApi.SamOpenUser(sre.domainHandle,
(UInt32)ObjectAccess.UserRead,
sre.RelativeId,
out userHandle);
ThrowOnFailure(status);
try
{
return MakeLocalUserObject(sre, userHandle);
}
finally
{
if (userHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(userHandle);
}
}
/// <summary>
/// Create a populated LocalUser object from a SamRidEnumeration object,
/// using an already-opened SAM user handle.
/// </summary>
/// <param name="sre">
/// A <see cref="SamRidEnumeration"/> object containing minimal information
/// about a local user.
/// </param>
/// <param name="userHandle">
/// Handle to an open SAM user.
/// </param>
/// <returns>
/// A LocalUser object, populated with user information.
/// </returns>
private LocalUser MakeLocalUserObject(SamRidEnumeration sre, IntPtr userHandle)
{
IntPtr buffer = IntPtr.Zero;
UInt32 status = 0;
try
{
USER_ALL_INFORMATION allInfo;
status = SamApi.SamQueryInformationUser(userHandle,
USER_INFORMATION_CLASS.UserAllInformation,
out buffer);
ThrowOnFailure(status);
allInfo = Marshal.PtrToStructure<USER_ALL_INFORMATION>(buffer);
var userSid = RidToSid(sre.domainHandle, sre.RelativeId);
LocalUser user = new LocalUser()
{
PrincipalSource = GetPrincipalSource(sre),
SID = userSid,
Name = allInfo.UserName.ToString(),
FullName = allInfo.FullName.ToString(),
Description = allInfo.AdminComment.ToString(),
// TODO: why is this coming up as 864000000000 (number of ticks per day)?
PasswordChangeableDate = DateTimeFromSam(allInfo.PasswordCanChange.QuadPart),
PasswordExpires = DateTimeFromSam(allInfo.PasswordMustChange.QuadPart),
// TODO: why is this coming up as 0X7FFFFFFFFFFFFFFF (largest signed 64-bit, and well out of range of DateTime)?
AccountExpires = DateTimeFromSam(allInfo.AccountExpires.QuadPart),
LastLogon = DateTimeFromSam(allInfo.LastLogon.QuadPart),
PasswordLastSet = DateTimeFromSam(allInfo.PasswordLastSet.QuadPart),
UserMayChangePassword = GetUserMayChangePassword(userHandle, userSid),
PasswordRequired = (allInfo.UserAccountControl & SamApi.USER_PASSWORD_NOT_REQUIRED) == 0,
Enabled = !((allInfo.UserAccountControl & SamApi.USER_ACCOUNT_DISABLED) == SamApi.USER_ACCOUNT_DISABLED)
};
return user;
}
finally
{
if (buffer != IntPtr.Zero)
status = SamApi.SamFreeMemory(buffer);
}
}
/// <summary>
/// Enable or disable a user.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the user to be
/// enabled or disabled.
/// </param>
/// <param name="enable">
/// One of the <see cref="Enabling"/> enumeration values indicating
/// whether the user is to be enabled or disabled.
/// </param>
private void EnableUser(SecurityIdentifier sid, Enabling enable)
{
IntPtr userHandle = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero;
UInt32 status = 0;
var sre = GetUserSre(sid);
status = SamApi.SamOpenUser(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out userHandle);
ThrowOnFailure(status);
try
{
USER_ALL_INFORMATION info;
status = SamApi.SamQueryInformationUser(userHandle,
USER_INFORMATION_CLASS.UserAllInformation,
out buffer);
ThrowOnFailure(status);
info = Marshal.PtrToStructure<USER_ALL_INFORMATION>(buffer);
status = SamApi.SamFreeMemory(buffer);
buffer = IntPtr.Zero;
UInt32 uac = info.UserAccountControl;
UInt32 enabled_state = uac & SamApi.USER_ACCOUNT_DISABLED;
if (enable == Enabling.Enable && enabled_state == SamApi.USER_ACCOUNT_DISABLED)
uac &= ~SamApi.USER_ACCOUNT_DISABLED;
else if (enable == Enabling.Disable && enabled_state != SamApi.USER_ACCOUNT_DISABLED)
uac |= SamApi.USER_ACCOUNT_DISABLED;
else
return;
if (uac != info.UserAccountControl)
{
info.UserAccountControl = uac;
info.WhichFields = SamApi.USER_ALL_USERACCOUNTCONTROL;
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(info));
Marshal.StructureToPtr(info, buffer, false);
status = SamApi.SamSetInformationUser(userHandle,
USER_INFORMATION_CLASS.UserAllInformation,
buffer);
Marshal.DestroyStructure<USER_ALL_INFORMATION>(buffer);
Marshal.FreeHGlobal(buffer);
buffer = IntPtr.Zero;
ThrowOnFailure(status);
}
}
finally
{
if (buffer != IntPtr.Zero)
Marshal.FreeHGlobal(buffer);
if (userHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(userHandle);
}
}
/// <summary>
/// Rename a user.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the user to be
/// renamed.
/// </param>
/// <param name="newName">The new user name.</param>
private void RenameUser(SecurityIdentifier sid, string newName)
{
IntPtr userHandle = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero;
UInt32 status = 0;
var sre = GetUserSre(sid);
status = SamApi.SamOpenUser(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out userHandle);
ThrowOnFailure(status);
try
{
USER_ACCOUNT_NAME_INFORMATION info = new USER_ACCOUNT_NAME_INFORMATION();
info.UserName = new UNICODE_STRING(newName);
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(info));
Marshal.StructureToPtr(info, buffer, false);
status = SamApi.SamSetInformationUser(userHandle,
USER_INFORMATION_CLASS.UserAccountNameInformation,
buffer);
ThrowOnFailure(status,
new Context {
objectId = newName,
operation = context.operation,
type = context.type
}
);
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.DestroyStructure<USER_ACCOUNT_NAME_INFORMATION>(buffer);
Marshal.FreeHGlobal(buffer);
}
if (userHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(userHandle);
}
}
/// <summary>
/// Delete a user.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the user to be
/// removed.
/// </param>
private void RemoveUser(SecurityIdentifier sid)
{
IntPtr userHandle = IntPtr.Zero;
var sre = GetUserSre(sid);
UInt32 status;
try
{
status = SamApi.SamOpenUser(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out userHandle);
ThrowOnFailure(status);
status = SamApi.SamDeleteUser(userHandle);
ThrowOnFailure(status);
userHandle = IntPtr.Zero; // The handle is freed internally if SamDeleteUser succeeds
}
finally
{
if (userHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(userHandle);
}
}
/// <summary>
/// Enumerate local users with native SAM functions.
/// </summary>
/// <param name="domainHandle">Handle to the domain to enumerate over.</param>
/// <returns>
/// An IEnumerable of SamRidEnumeration objects, one for each local user.
/// </returns>
/// <remarks>
/// This is a "generator" method. Rather than returning an entire collection,
/// it uses 'yield return' to return each object in turn.
/// </remarks>
private static IEnumerable<SamRidEnumeration> EnumerateGroupsInDomain(IntPtr domainHandle)
{
UInt32 status = 0;
UInt32 context = 0;
IntPtr buffer = IntPtr.Zero;
UInt32 countReturned;
do
{
// Although the method name indicates that we are operating with "groups",
// it actually uses the SAM API's SamEnumerateAliasesInDomain function.
status = SamApi.SamEnumerateAliasesInDomain(domainHandle,
ref context,
out buffer,
1,
out countReturned);
if (status == NtStatus.STATUS_MORE_ENTRIES && countReturned == 1)
{
if (buffer != IntPtr.Zero)
{
SAM_RID_ENUMERATION sre;
sre = Marshal.PtrToStructure<SAM_RID_ENUMERATION>(buffer);
SamApi.SamFreeMemory(buffer);
buffer = IntPtr.Zero;
yield return new SamRidEnumeration
{
Name = sre.Name.ToString(),
RelativeId = sre.RelativeId,
domainHandle = domainHandle
};
}
}
} while (Succeeded(status) && status != 0 && countReturned != 0);
}
/// <summary>
/// Enumerate group objects in both the local and builtin domains.
/// </summary>
/// <returns>
/// An IEnumerable of SamRidEnumeration objects, one for each local group.
/// </returns>
/// <remarks>
/// This is a "generator" method. Rather than returning an entire collection,
/// it uses 'yield return' to return each object in turn.
/// </remarks>
internal IEnumerable<SamRidEnumeration> EnumerateGroups()
{
foreach (var sre in EnumerateGroupsInDomain(localDomainHandle))
yield return sre;
foreach (var sre in EnumerateGroupsInDomain(builtinDomainHandle))
yield return sre;
}
/// <summary>
/// Create a new group in the specified domain.
/// </summary>
/// <param name="groupInfo">
/// A <see cref="LocalGroup"/> object containing information about the new group.
/// </param>
/// <param name="domainHandle">Handle to the domain in which to create the new group.</param>
/// <returns>
/// A LocalGroup object that represents the newly-created group.
/// </returns>
private LocalGroup CreateGroup(LocalGroup groupInfo, IntPtr domainHandle)
{
IntPtr aliasHandle = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero;
UNICODE_STRING str = new UNICODE_STRING();
UInt32 status;
try
{
UInt32 relativeId;
str = new UNICODE_STRING(groupInfo.Name);
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(str));
Marshal.StructureToPtr(str, buffer, false);
status = SamApi.SamCreateAliasInDomain(domainHandle,
buffer,
Win32.MAXIMUM_ALLOWED,
out aliasHandle,
out relativeId);
Marshal.DestroyStructure<UNICODE_STRING>(buffer);
Marshal.FreeHGlobal(buffer);
buffer = IntPtr.Zero;
ThrowOnFailure(status);
if (!string.IsNullOrEmpty(groupInfo.Description))
{
ALIAS_ADM_COMMENT_INFORMATION info = new ALIAS_ADM_COMMENT_INFORMATION();
info.AdminComment = new UNICODE_STRING(groupInfo.Description);
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(info));
Marshal.StructureToPtr(info, buffer, false);
status = SamApi.SamSetInformationAlias(aliasHandle,
ALIAS_INFORMATION_CLASS.AliasAdminCommentInformation,
buffer);
Marshal.DestroyStructure<ALIAS_ADM_COMMENT_INFORMATION>(buffer);
Marshal.FreeHGlobal(buffer);
buffer = IntPtr.Zero;
ThrowOnFailure(status);
}
return MakeLocalGroupObject(new SamRidEnumeration
{
domainHandle = domainHandle,
Name = groupInfo.Name,
RelativeId = relativeId
},
aliasHandle);
}
finally
{
if (buffer != IntPtr.Zero)
Marshal.FreeHGlobal(buffer);
if (aliasHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(aliasHandle);
}
}
/// <summary>
/// Update a local group with new property values. This method provides
/// the actual implementation.
/// </summary>
/// <param name="group">
/// A <see cref="LocalGroup"/> object representing the group to be updated.
/// </param>
/// <param name="changed">
/// A LocalGroup object containing the desired changes.
/// </param>
/// <remarks>
/// Currently, a group's description is the only changeable property.
/// </remarks>
private void UpdateGroup(LocalGroup group, LocalGroup changed)
{
// Only description may be changed
if (group.Description == changed.Description)
return;
IntPtr aliasHandle = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero;
if (group.SID == null)
group = GetLocalGroup(group.Name);
var sre = GetGroupSre(group.SID);
UInt32 status;
try
{
status = SamApi.SamOpenAlias(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out aliasHandle);
ThrowOnFailure(status);
ALIAS_ADM_COMMENT_INFORMATION info = new ALIAS_ADM_COMMENT_INFORMATION();
info.AdminComment = new UNICODE_STRING(changed.Description);
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(info));
Marshal.StructureToPtr(info, buffer, false);
status = SamApi.SamSetInformationAlias(aliasHandle,
ALIAS_INFORMATION_CLASS.AliasAdminCommentInformation,
buffer);
ThrowOnFailure(status);
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.DestroyStructure<ALIAS_ADM_COMMENT_INFORMATION>(buffer);
Marshal.FreeHGlobal(buffer);
}
if (aliasHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(aliasHandle);
}
}
/// <summary>
/// Create a populated LocalGroup object from a SamRidEnumeration object.
/// </summary>
/// <param name="sre">
/// A <see cref="SamRidEnumeration"/> object containing minimal information
/// about a local group.
/// </param>
/// <returns>
/// A LocalGroup object, populated with group information.
/// </returns>
private LocalGroup MakeLocalGroupObject(SamRidEnumeration sre)
{
IntPtr aliasHandle = IntPtr.Zero;
var status = SamApi.SamOpenAlias(sre.domainHandle,
Win32.MAXIMUM_ALLOWED,
sre.RelativeId,
out aliasHandle);
ThrowOnFailure(status);
try
{
return MakeLocalGroupObject(sre, aliasHandle);
}
finally
{
if (aliasHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(aliasHandle);
}
}
/// <summary>
/// Create a populated LocalGroup object from a SamRidEnumeration object,
/// using an already-opened SAM alias handle.
/// </summary>
/// <param name="sre">
/// A <see cref="SamRidEnumeration"/> object containing minimal information
/// about a local group.
/// </param>
/// <param name="aliasHandle">
/// Handle to an open SAM alias.
/// </param>
/// <returns>
/// A LocalGroup object, populated with group information.
/// </returns>
private LocalGroup MakeLocalGroupObject(SamRidEnumeration sre, IntPtr aliasHandle)
{
IntPtr buffer = IntPtr.Zero;
UInt32 status = 0;
try
{
ALIAS_GENERAL_INFORMATION generalInfo;
status = SamApi.SamQueryInformationAlias(aliasHandle,
ALIAS_INFORMATION_CLASS.AliasGeneralInformation,
out buffer);
ThrowOnFailure(status);
generalInfo = Marshal.PtrToStructure<ALIAS_GENERAL_INFORMATION>(buffer);
LocalGroup group = new LocalGroup()
{
PrincipalSource = GetPrincipalSource(sre),
SID = RidToSid(sre.domainHandle, sre.RelativeId),
Name = generalInfo.Name.ToString(),
Description = generalInfo.AdminComment.ToString()
};
return group;
}
finally
{
if (buffer != IntPtr.Zero)
status = SamApi.SamFreeMemory(buffer);
}
}
/// <summary>
/// Update a local user with new properties.
/// </summary>
/// <param name="user">
/// A <see cref="LocalUser"/> object representing the user to be updated.
/// </param>
/// <param name="changed">
/// A LocalUser object containing the desired changes.
/// </param>
/// <param name="password">A <see cref="System.Security.SecureString"/>
/// object containing the new password. A null value in this parameter
/// indicates that the password is not to be changed.
/// </param>
/// <param name="passwordExpired">One of the
/// <see cref="PasswordExpiredState"/> enumeration values indicating
/// whether the password-expired state is to be explicitly set or
/// left as is.
/// If the <paramref name="password"/> parameter is null, this parameter
/// is ignored.
/// </param>
/// <param name="setPasswordNeverExpires">
/// Indicates whether the PasswordNeverExpires parameter was specified.
/// </param>
private void UpdateUser(LocalUser user,
LocalUser changed,
System.Security.SecureString password,
PasswordExpiredState passwordExpired,
bool? setPasswordNeverExpires)
{
UserProperties properties = UserProperties.None;
if (user.AccountExpires != changed.AccountExpires)
properties |= UserProperties.AccountExpires;
if (user.Description != changed.Description)
properties |= UserProperties.Description;
if (user.FullName != changed.FullName)
properties |= UserProperties.FullName;
if (setPasswordNeverExpires.HasValue)
properties |= UserProperties.PasswordNeverExpires;
if (user.UserMayChangePassword != changed.UserMayChangePassword)
properties |= UserProperties.UserMayChangePassword;
if (user.PasswordRequired != changed.PasswordRequired)
properties |= UserProperties.PasswordRequired;
if ( properties != UserProperties.None
|| passwordExpired != PasswordExpiredState.Unchanged
|| password != null)
{
IntPtr userHandle = IntPtr.Zero;
UInt32 status = 0;
try
{
status = SamApi.SamOpenUser(localDomainHandle,
Win32.MAXIMUM_ALLOWED,
user.SID.GetRid(),
out userHandle);
ThrowOnFailure(status);
SetUserData(userHandle, changed, properties, password, passwordExpired, setPasswordNeverExpires);
}
finally
{
if (userHandle != IntPtr.Zero)
status = SamApi.SamCloseHandle(userHandle);
}
}
}
/// <summary>
/// Set selected properties of a user.
/// </summary>
/// <param name="userHandle">Handle to an open SAM user.</param>
/// <param name="sourceUser">
/// A <see cref="LocalUser"/> object containing the data to set into the user.
/// </param>
/// <param name="setFlags">
/// A combination of <see cref="UserProperties"/> values indicating the properties to be set.
/// </param>
/// <param name="password">A <see cref="System.Security.SecureString"/>
/// object containing the new password.
/// </param>
/// <param name="passwordExpired">One of the
/// <see cref="PasswordExpiredState"/> enumeration values indicating
/// whether the password-expired state is to be explicitly set or
/// left as is. If the <paramref name="password"/> parameter is null,
/// this parameter is ignored.
/// </param>
/// <param name="setPasswordNeverExpires">
/// Nullable value the specifies whether the PasswordNeverExpires bit should be flipped
/// </param>
private void SetUserData(IntPtr userHandle,
LocalUser sourceUser,
UserProperties setFlags,
System.Security.SecureString password,
PasswordExpiredState passwordExpired,
bool? setPasswordNeverExpires)
{
IntPtr buffer = IntPtr.Zero;
try
{
UInt32 which = 0;
UInt32 status = 0;
UInt32 uac = GetUserAccountControl(userHandle);
USER_ALL_INFORMATION info = new USER_ALL_INFORMATION();
if (setFlags.HasFlag(UserProperties.AccountExpires))
{
which |= SamApi.USER_ALL_ACCOUNTEXPIRES;
info.AccountExpires.QuadPart = sourceUser.AccountExpires.HasValue
? sourceUser.AccountExpires.Value.ToFileTime()
: 0L;
}
if (setFlags.HasFlag(UserProperties.Description))
{
which |= SamApi.USER_ALL_ADMINCOMMENT;
info.AdminComment = new UNICODE_STRING(sourceUser.Description);
}
if (setFlags.HasFlag(UserProperties.Enabled))
{
which |= SamApi.USER_ALL_USERACCOUNTCONTROL;
if (sourceUser.Enabled)
uac &= ~SamApi.USER_ACCOUNT_DISABLED;
else
uac |= SamApi.USER_ACCOUNT_DISABLED;
}
if (setFlags.HasFlag(UserProperties.FullName))
{
which |= SamApi.USER_ALL_FULLNAME;
info.FullName = new UNICODE_STRING(sourceUser.FullName);
}
if (setFlags.HasFlag(UserProperties.PasswordNeverExpires))
{
// Only modify the bit if a change was requested
if (setPasswordNeverExpires.HasValue)
{
which |= SamApi.USER_ALL_USERACCOUNTCONTROL;
if (setPasswordNeverExpires.Value)
uac |= SamApi.USER_DONT_EXPIRE_PASSWORD;
else
uac &= ~SamApi.USER_DONT_EXPIRE_PASSWORD;
}
}
if (setFlags.HasFlag(UserProperties.PasswordRequired))
{
which |= SamApi.USER_ALL_USERACCOUNTCONTROL;
if (sourceUser.PasswordRequired)
uac &= ~SamApi.USER_PASSWORD_NOT_REQUIRED;
else
uac |= SamApi.USER_PASSWORD_NOT_REQUIRED;
}
if (which != 0)
{
info.WhichFields = which;
if ((which & SamApi.USER_ALL_USERACCOUNTCONTROL) != 0)
info.UserAccountControl = uac;
buffer = Marshal.AllocHGlobal(Marshal.SizeOf<USER_ALL_INFORMATION>());
Marshal.StructureToPtr<USER_ALL_INFORMATION>(info, buffer, false);
status = SamApi.SamSetInformationUser(userHandle,
USER_INFORMATION_CLASS.UserAllInformation,
buffer);
ThrowOnFailure(status);
status = SamApi.SamFreeMemory(buffer);
buffer = IntPtr.Zero;
}
if (setFlags.HasFlag(UserProperties.UserMayChangePassword))
SetUserMayChangePassword(userHandle, sourceUser.SID, sourceUser.UserMayChangePassword);
if (password != null)
SetUserPassword(userHandle, password, passwordExpired);
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.DestroyStructure<USER_ALL_INFORMATION>(buffer);
Marshal.FreeHGlobal(buffer);
}
}
}
/// <summary>
/// Retrieve the User's User Account Control flags.
/// </summary>
/// <param name="userHandle">
/// Handle to an open user.
/// </param>
/// <returns>
/// A 32-bit unsigned integer containing the User Account Control
/// flags as a set of bits.
/// </returns>
private UInt32 GetUserAccountControl(IntPtr userHandle)
{
IntPtr buffer = IntPtr.Zero;
USER_LOGON_INFORMATION info;
UInt32 status;
try
{
status = SamApi.SamQueryInformationUser(userHandle,
USER_INFORMATION_CLASS.UserLogonInformation,
out buffer);
ThrowOnFailure(status);
info = Marshal.PtrToStructure<USER_LOGON_INFORMATION>(buffer);
status = SamApi.SamFreeMemory(buffer);
buffer = IntPtr.Zero;
return info.UserAccountControl;
}
finally
{
if (buffer != IntPtr.Zero)
status = SamApi.SamFreeMemory(buffer);
}
}
/// <summary>
/// Retrieve the DACL from a SAM object.
/// </summary>
/// <param name="objectHandle">
/// A handle to the SAM object whose DACL is to be retrieved.
/// </param>
/// <returns>
/// A <see cref="RawAcl"/> object containing the DACL retrieved from
/// the SAM object.
/// </returns>
private RawAcl GetSamDacl(IntPtr objectHandle)
{
RawAcl rv = null;
IntPtr securityObject = IntPtr.Zero;
UInt32 status = 0;
try
{
status = SamApi.SamQuerySecurityObject(objectHandle, Win32.DACL_SECURITY_INFORMATION, out securityObject);
ThrowOnFailure(status);
SECURITY_DESCRIPTOR sd = Marshal.PtrToStructure<SECURITY_DESCRIPTOR>(securityObject);
bool daclPresent;
bool daclDefaulted;
IntPtr dacl;
bool ok = Win32.GetSecurityDescriptorDacl(securityObject, out daclPresent, out dacl, out daclDefaulted);
if (!ok)
{
var error = Marshal.GetLastWin32Error();
if (error == Win32.ERROR_ACCESS_DENIED)
throw new AccessDeniedException(context.target);
else
throw new Win32InternalException(error, context.target);
}
if (daclPresent)
{
ACL acl = Marshal.PtrToStructure<ACL>(dacl);
if (acl.AclSize != 0)
{
// put the DACL into managed data
var bytes = new byte[acl.AclSize];
Marshal.Copy(dacl, bytes, 0, acl.AclSize);
rv = new RawAcl(bytes, 0);
}
}
}
finally
{
if (IntPtr.Zero != securityObject)
status = SamApi.SamFreeMemory(securityObject);
}
return rv;
}
/// <summary>
/// Set the DACL of a SAM object.
/// </summary>
/// <param name="objectHandle">
/// A handle to the SAM object whose DACL is to be retrieved.
/// </param>
/// <param name="rawAcl">
/// A <see cref="RawAcl"/> object containing the DACL to be set into
/// the SAM object.
/// </param>
private void SetSamDacl(IntPtr objectHandle, RawAcl rawAcl)
{
IntPtr ipsd = IntPtr.Zero;
IntPtr ipDacl = IntPtr.Zero;
try
{
bool present = false;
// create a new security descriptor
var sd = new SECURITY_DESCRIPTOR() { Revision = 1 };
ipsd = Marshal.AllocHGlobal(Marshal.SizeOf<SECURITY_DESCRIPTOR>());
if (rawAcl != null && rawAcl.BinaryLength > 0)
{
Marshal.StructureToPtr<SECURITY_DESCRIPTOR>(sd, ipsd, false);
// put the DACL into unmanaged memory
var length = rawAcl.BinaryLength;
var bytes = new byte[length];
rawAcl.GetBinaryForm(bytes, 0);
ipDacl = Marshal.AllocHGlobal(length);
Marshal.Copy(bytes, 0, ipDacl, length);
present = true;
}
// set the DACL into our new security descriptor
var ok = Win32.SetSecurityDescriptorDacl(ipsd, present, ipDacl, false);
if (!ok)
{
var error = Marshal.GetLastWin32Error();
if (error == Win32.ERROR_ACCESS_DENIED)
throw new AccessDeniedException(context.target);
else
throw new Win32InternalException(error, context.target);
}
var status = SamApi.SamSetSecurityObject(objectHandle, Win32.DACL_SECURITY_INFORMATION, ipsd);
ThrowOnFailure(status);
}
finally
{
Marshal.FreeHGlobal(ipDacl);
Marshal.FreeHGlobal(ipsd);
}
}
/// <summary>
/// Determine if a user account password may be changed by the user.
/// </summary>
/// <param name="userHandle">
/// Handle to a SAM user object.
/// </param>
/// <param name="userSid">
/// A <see cref="SecurityIdentifier"/> object identifying the SAM
/// object's associated user.
/// </param>
/// <returns>
/// True if the user account password may be changed by its user,
/// false otherwise.
/// </returns>
/// <remarks>
/// The ability to for the user to change the user account password
/// is a permission in the object's DACL. This method walks through
/// the ACEs in the DACL, checking if the permission is granted to
/// either Everyone or the user identified by the userSid parameter.
/// </remarks>
private bool GetUserMayChangePassword(IntPtr userHandle, SecurityIdentifier userSid)
{
var rawAcl = GetSamDacl(userHandle);
// if there is no DACL, then access is granted
if (rawAcl == null)
return true;
foreach (var a in rawAcl)
{
var ace = a as CommonAce;
if (ace != null && ace.AceType == AceType.AccessAllowed)
{
if (ace.SecurityIdentifier == worldSid ||
ace.SecurityIdentifier == userSid)
{
if ((ace.AccessMask & SamApi.USER_CHANGE_PASSWORD) != 0)
return true;
}
}
}
return false;
}
/// <summary>
/// Set whether a user account password may be changed by the user.
/// </summary>
/// <param name="userHandle">
/// Handle to a SAM user object.
/// </param>
/// <param name="userSid">
/// A <see cref="SecurityIdentifier"/> object identifying the SAM
/// object's associated user.
/// </param>
/// <param name="enable">
/// A boolean indicating whether the permission is to be enabled or
/// disabled.
/// </param>
/// <remarks>
/// The ability to for the user to change the user account password
/// is a permission in the object's DACL. This method walks through
/// the ACEs in the DACL, enabling or disabling the permission on ACEs
/// associated with either Everyone or the user identified by the
/// userSid parameter.
/// </remarks>
private void SetUserMayChangePassword(IntPtr userHandle, SecurityIdentifier userSid, bool enable)
{
var changed = false;
var rawAcl = GetSamDacl(userHandle);
if (rawAcl != null)
{
foreach (var a in rawAcl)
{
var ace = a as CommonAce;
if (ace != null && ace.AceType == AceType.AccessAllowed)
{
if (ace.SecurityIdentifier == worldSid ||
ace.SecurityIdentifier == userSid)
{
if (enable)
ace.AccessMask |= SamApi.USER_CHANGE_PASSWORD;
else
ace.AccessMask &= ~SamApi.USER_CHANGE_PASSWORD;
changed = true;
}
}
}
if (changed)
SetSamDacl(userHandle, rawAcl);
}
}
/// <summary>
/// Determine if a user's password has expired.
/// </summary>
/// <param name="userHandle">
/// Handle to an open User.
/// </param>
/// <returns>
/// True if the user's password has expired, false otherwise.
/// </returns>
private bool IsPasswordExpired(IntPtr userHandle)
{
IntPtr buffer = IntPtr.Zero;
USER_ALL_INFORMATION info;
UInt32 status;
try
{
status = SamApi.SamQueryInformationUser(userHandle,
USER_INFORMATION_CLASS.UserAllInformation,
out buffer);
ThrowOnFailure(status);
info = Marshal.PtrToStructure<USER_ALL_INFORMATION>(buffer);
status = SamApi.SamFreeMemory(buffer);
buffer = IntPtr.Zero;
return info.PasswordExpired;
}
finally
{
if (buffer != IntPtr.Zero)
status = SamApi.SamFreeMemory(buffer);
}
}
/// <summary>
/// Set a user's password.
/// </summary>
/// <param name="userHandle">Handle to an open User.</param>
/// <param name="password">A <see cref="System.Security.SecureString"/>
/// object containing the new password.
/// </param>
/// <param name="passwordExpired">One of the
/// <see cref="PasswordExpiredState"/> enumeration values indicating
/// whether the password-expired state is to be explicitly set or
/// left as is.
/// </param>
private void SetUserPassword(IntPtr userHandle,
System.Security.SecureString password,
PasswordExpiredState passwordExpired)
{
if (password != null)
{
USER_SET_PASSWORD_INFORMATION info = new USER_SET_PASSWORD_INFORMATION();
IntPtr buffer = IntPtr.Zero;
try
{
bool setPwExpire = false;
switch (passwordExpired)
{
case PasswordExpiredState.Expired:
setPwExpire = true;
break;
case PasswordExpiredState.NotExpired:
setPwExpire = false;
break;
case PasswordExpiredState.Unchanged:
setPwExpire = IsPasswordExpired(userHandle);
break;
}
info.Password = new UNICODE_STRING(password.AsString());
info.PasswordExpired = setPwExpire;
buffer = Marshal.AllocHGlobal(Marshal.SizeOf(info));
Marshal.StructureToPtr<USER_SET_PASSWORD_INFORMATION>(info, buffer, false);
var status = SamApi.SamSetInformationUser(userHandle,
USER_INFORMATION_CLASS.UserSetPasswordInformation,
buffer);
ThrowOnFailure(status);
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.DestroyStructure<USER_SET_PASSWORD_INFORMATION>(buffer);
Marshal.FreeHGlobal(buffer);
}
}
}
}
#region Utility Methods
/// <summary>
/// Create a <see cref="System.Security.Principal.SecurityIdentifier"/>
/// object from a relative ID.
/// </summary>
/// <param name="domainHandle">
/// Handle to the domain from which the ID was acquired.
/// </param>
/// <param name="rid">
/// The Relative ID value.
/// </param>
/// <returns>
/// A SecurityIdentifier object containing the SID of the
/// object identified by the <paramref name="rid"/> parameter.
/// </returns>
private SecurityIdentifier RidToSid(IntPtr domainHandle, uint rid)
{
IntPtr sidBytes = IntPtr.Zero;
UInt32 status = 0;
SecurityIdentifier sid = null;
try
{
status = SamApi.SamRidToSid(domainHandle, rid, out sidBytes);
if (status == NtStatus.STATUS_NOT_FOUND)
throw new InternalException(status,
StringUtil.Format(Strings.RidToSidFailed, rid),
ErrorCategory.ObjectNotFound);
ThrowOnFailure(status);
sid = new SecurityIdentifier(sidBytes);
}
finally
{
if (IntPtr.Zero != sidBytes)
status = SamApi.SamFreeMemory(sidBytes);
}
return sid;
}
/// <summary>
/// Lookup the account identified by the specified SID.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the account
/// to look up.
/// </param>
/// <returns>
/// A <see cref="AccountInfo"/> object contains information about the
/// account, or null if no matching account was found.
/// </returns>
private AccountInfo LookupAccountInfo(SecurityIdentifier sid)
{
var sbAccountName = new StringBuilder();
var sbDomainName = new StringBuilder();
var accountNameLength = sbAccountName.Capacity;
var domainNameLength = sbDomainName.Capacity;
SID_NAME_USE use;
var error = Win32.NO_ERROR;
var bytes = new byte[sid.BinaryLength];
sid.GetBinaryForm(bytes, 0);
if (!Win32.LookupAccountSid(null,
bytes,
sbAccountName, ref accountNameLength,
sbDomainName, ref domainNameLength,
out use))
{
error = Marshal.GetLastWin32Error();
if (error == Win32.ERROR_INSUFFICIENT_BUFFER)
{
sbAccountName.EnsureCapacity(accountNameLength);
sbDomainName.EnsureCapacity((int)domainNameLength);
error = Win32.NO_ERROR;
if (!Win32.LookupAccountSid(null,
bytes,
sbAccountName, ref accountNameLength,
sbDomainName, ref domainNameLength,
out use))
error = Marshal.GetLastWin32Error();
}
}
if (error == Win32.ERROR_SUCCESS)
return new AccountInfo
{
AccountName = sbAccountName.ToString(),
DomainName = sbDomainName.ToString(),
Sid = sid,
Use = use
};
else if (error == Win32.ERROR_NONE_MAPPED)
return null;
else
throw new Win32InternalException(error, context.target);
}
/// <summary>
/// Lookup the account identified by specified account name.
/// </summary>
/// <param name="accountName">
/// A string containing the name of the account to look up.
/// </param>
/// <returns>
/// A <see cref="AccountInfo"/> object contains information about the
/// account, or null if no matching account was found.
/// </returns>
private AccountInfo LookupAccountInfo(string accountName)
{
var sbDomainName = new StringBuilder();
var domainNameLength = (uint)sbDomainName.Capacity;
byte [] sid = null;
uint sidLength = 0;
SID_NAME_USE use;
int error = Win32.NO_ERROR;
if (!Win32.LookupAccountName(null,
accountName,
sid,
ref sidLength,
sbDomainName,
ref domainNameLength,
out use))
{
error = Marshal.GetLastWin32Error();
if (error == Win32.ERROR_INSUFFICIENT_BUFFER || error == Win32.ERROR_INVALID_FLAGS)
{
sid = new byte[sidLength];
sbDomainName.EnsureCapacity((int)domainNameLength);
error = Win32.NO_ERROR;
if (!Win32.LookupAccountName(null,
accountName,
sid,
ref sidLength,
sbDomainName,
ref domainNameLength,
out use))
error = Marshal.GetLastWin32Error();
}
}
if (error == Win32.ERROR_SUCCESS)
{
// Bug: 7407413 :
// If accountname is in the format domain1\user1,
// then AccountName.ToString() will return domain1\domain1\user1
// Ideally , accountname should be processed to hold only account name (without domain)
// as we are keeping the domain in 'DomainName' variable.
int index = accountName.IndexOf("\\", StringComparison.CurrentCultureIgnoreCase);
if (index > -1)
{
accountName = accountName.Substring(index + 1);
}
return new AccountInfo
{
AccountName = accountName,
DomainName = sbDomainName.ToString(),
Sid = new SecurityIdentifier(sid, 0),
Use = use
};
}
else if (error == Win32.ERROR_NONE_MAPPED)
return null;
else if (error == Win32.ERROR_ACCESS_DENIED)
throw new AccessDeniedException(context.target);
else
throw new Win32InternalException(error, context.target);
}
/// <summary>
/// Create a <see cref="LocalPrincipal"/> object from information in
/// an AccountInfo object.
/// </summary>
/// <param name="info">
/// An AccountInfo object containing information about the account
/// for which the LocalPrincipal object is being created. This parameter
/// may be null, in which case this method returns null.
/// </param>
/// <returns>
/// A new LocalPrincipal object representing the account, or null if the
/// <paramref name="info"/> parameter is null.
/// </returns>
private LocalPrincipal MakeLocalPrincipalObject(AccountInfo info)
{
if (info == null)
return null; // this is a legitimate case
var rv = new LocalPrincipal(info.ToString());
rv.SID = info.Sid;
rv.PrincipalSource = GetPrincipalSource(info);
switch (info.Use)
{
case SID_NAME_USE.SidTypeAlias: // TODO: is this the right thing to do???
case SID_NAME_USE.SidTypeGroup:
case SID_NAME_USE.SidTypeWellKnownGroup:
rv.ObjectClass = Strings.ObjectClassGroup;
break;
case SID_NAME_USE.SidTypeUser:
rv.ObjectClass = Strings.ObjectClassUser;
break;
default:
rv.ObjectClass = Strings.ObjectClassOther;
break;
}
return rv;
}
/// <summary>
/// Indicate whether a Status code is a successful value.
/// </summary>
/// <param name="ntStatus">
/// One of the NTSTATUS code values indicating the error, if any.
/// </param>
/// <returns>
/// True if the Status code represents a success, false otherwise.
/// </returns>
private static bool Succeeded(UInt32 ntStatus)
{
return NtStatus.IsSuccess(ntStatus);
}
/// <summary>
/// Helper to throw an exception if the provided Status code
/// represents a failure.
/// </summary>
/// <param name="ntStatus">
/// One of the NTSTATUS code values indicating the error, if any.
/// </param>
/// <param name="context">
/// A <see cref="Context"/> object containing information about the
/// current operation. If this parameter is null, the class's context
/// is used.
/// </param>
private void ThrowOnFailure(UInt32 ntStatus, Context context = null)
{
if (NtStatus.IsError(ntStatus))
{
var ex = MakeException(ntStatus, context);
if (ex != null)
throw ex;
}
}
/// <summary>
/// Create an appropriate exception from the specified status code.
/// </summary>
/// <param name="ntStatus">
/// One of the NTSTATUS code values indicating the error, if any.
/// </param>
/// <param name="context">
/// A <see cref="Context"/> object containing information about the
/// current operation. If this parameter is null, the class's context
/// is used.
/// </param>
/// <returns>
/// An <see cref="Exception"/> object, or an object derived from Exception,
/// appropriate to the error. If <paramref name="ntStatus"/> does not
/// indicate an error, the method returns null.
/// </returns>
private Exception MakeException(UInt32 ntStatus, Context context = null)
{
if (!NtStatus.IsError(ntStatus))
return null;
if (context == null)
context = this.context;
switch (ntStatus)
{
case NtStatus.STATUS_ACCESS_DENIED:
return new AccessDeniedException(context.target);
case NtStatus.STATUS_INVALID_ACCOUNT_NAME:
return new InvalidNameException(context.ObjectName, context.target);
case NtStatus.STATUS_USER_EXISTS:
if (context.operation == ContextOperation.New &&
context.type == ContextObjectType.User)
{
return new UserExistsException(context.ObjectName, context.target);
}
else
{
return new NameInUseException(context.ObjectName, context.target);
}
case NtStatus.STATUS_ALIAS_EXISTS:
if (context.operation == ContextOperation.New &&
context.type == ContextObjectType.Group)
{
return new GroupExistsException(context.ObjectName, context.target);
}
else
{
return new NameInUseException(context.ObjectName, context.target);
}
case NtStatus.STATUS_GROUP_EXISTS:
return new NameInUseException(context.ObjectName, context.target);
case NtStatus.STATUS_NO_SUCH_ALIAS:
case NtStatus.STATUS_NO_SUCH_GROUP:
return new GroupNotFoundException(context.ObjectName, context.target);
case NtStatus.STATUS_NO_SUCH_USER:
return new UserNotFoundException(context.ObjectName, context.target);
case NtStatus.STATUS_SPECIAL_GROUP: // The group specified is a special group and cannot be operated on in the requested fashion.
// case NtStatus.STATUS_SPECIAL_ALIAS: // referred to in source for SAM api, but not in ntstatus.h!!!
return new InvalidOperationException(StringUtil.Format(Strings.InvalidForGroup, context.ObjectName));
case NtStatus.STATUS_SPECIAL_USER: // The user specified is a special user and cannot be operated on in the requested fashion.
return new InvalidOperationException(StringUtil.Format(Strings.InvalidForUser, context.ObjectName));
case NtStatus.STATUS_NO_SUCH_MEMBER:
return new MemberNotFoundException(context.MemberName, context.ObjectName);
case NtStatus.STATUS_MEMBER_IN_ALIAS:
case NtStatus.STATUS_MEMBER_IN_GROUP:
if (context.operation == ContextOperation.Remove &&
context.type == ContextObjectType.Group)
{
return new InvalidOperationException(StringUtil.Format(Strings.GroupHasMembers, context.ObjectName));
}
else
{
return new MemberExistsException(context.MemberName, context.ObjectName, context.target);
}
case NtStatus.STATUS_MEMBER_NOT_IN_ALIAS:
case NtStatus.STATUS_MEMBER_NOT_IN_GROUP:
return new MemberNotFoundException(context.MemberName, context.ObjectName);
case NtStatus.STATUS_MEMBERS_PRIMARY_GROUP:
return new InvalidOperationException(StringUtil.Format(Strings.MembersPrimaryGroup, context.ObjectName));
case NtStatus.STATUS_LAST_ADMIN: // Cannot delete the last administrator.
return new InvalidOperationException(Strings.LastAdmin);
case NtStatus.STATUS_ILL_FORMED_PASSWORD:
case NtStatus.STATUS_PASSWORD_RESTRICTION:
return new InvalidPasswordException(Native.Win32.RtlNtStatusToDosError(ntStatus));
// TODO: do we want to handle these?
// they appear to be returned only in functions we are not calling
case NtStatus.STATUS_INVALID_SID: // member sid is corrupted
case NtStatus.STATUS_INVALID_MEMBER: // member has wrong account type
default:
return new InternalException(ntStatus, context.target);
}
}
/// <summary>
/// Create a DateTime object from a 64-bit value from one of the SAM
/// structures.
/// </summary>
/// <param name="samValue">
/// A signed 64-bit value representing a date and time.
/// </param>
/// <returns>
/// A nullable DateTime object representing a date and time,
/// or null if the <paramref name="samValue"/> parameter is zero.
/// </returns>
private static DateTime? DateTimeFromSam(Int64 samValue)
{
if (samValue == 0 || samValue == 0X7FFFFFFFFFFFFFFF)
return null;
return DateTime.FromFileTime(samValue);
}
/// <summary>
/// Determine the source of a user or group. Either local, Active Directory,
/// or Azure AD.
/// </summary>
/// <param name="sid">
/// A <see cref="SecurityIdentifier"/> object identifying the user or group.
/// </param>
/// <returns>
/// One of the <see cref="PrincipalSource"/> enumerations identifying the
/// source of the object.
/// </returns>
private PrincipalSource? GetPrincipalSource(SecurityIdentifier sid)
{
var bSid = new byte[sid.BinaryLength];
sid.GetBinaryForm(bSid, 0);
var type = LSA_USER_ACCOUNT_TYPE.UnknownUserAccountType;
// Use LsaLookupUserAccountType for Windows 10 and later.
// Earlier versions of the OS will leave the property NULL because
// it is too error prone to attempt to replicate the decisions of
// LsaLookupUserAccountType.
var os = GetOperatingSystem();
if (os.Version.Major >= 10)
{
UInt32 status = Native.Win32.LsaLookupUserAccountType(bSid, out type);
if (NtStatus.IsError(status))
type = LSA_USER_ACCOUNT_TYPE.UnknownUserAccountType;
switch (type)
{
case LSA_USER_ACCOUNT_TYPE.ExternalDomainUserAccountType:
case LSA_USER_ACCOUNT_TYPE.PrimaryDomainUserAccountType:
return PrincipalSource.ActiveDirectory;
case LSA_USER_ACCOUNT_TYPE.LocalUserAccountType:
return PrincipalSource.Local;
case LSA_USER_ACCOUNT_TYPE.AADUserAccountType:
return PrincipalSource.AzureAD;
// Currently, there is no value returned by LsaLookupUserAccountType
// that corresponds to LSA_USER_ACCOUNT_TYPE.MSAUserAccountType,
// but there may be in the future, so we'll account for it here.
case LSA_USER_ACCOUNT_TYPE.MSAUserAccountType:
case LSA_USER_ACCOUNT_TYPE.LocalConnectedUserAccountType:
return PrincipalSource.MicrosoftAccount;
case LSA_USER_ACCOUNT_TYPE.InternetUserAccountType:
return sid.IsMsaAccount()
? PrincipalSource.MicrosoftAccount
: PrincipalSource.Unknown;
case LSA_USER_ACCOUNT_TYPE.UnknownUserAccountType:
default:
return PrincipalSource.Unknown;
}
}
else
{
return null;
}
}
/// <summary>
/// Determine the source of a user or group. Either local, Active Directory,
/// or Azure AD.
/// </summary>
/// <param name="info">
/// An <see cref="AccountInfo"/> object containing information about the
/// user or group.
/// </param>
/// <returns>
/// One of the <see cref="PrincipalSource"/> enumerations identifying the
/// source of the object.
/// </returns>
private PrincipalSource? GetPrincipalSource(AccountInfo info)
{
return GetPrincipalSource(info.Sid);
}
/// <summary>
/// Determine the source of a user or group. Either local, Active Directory,
/// or Azure AD.
/// </summary>
/// <param name="sre">
/// A <see cref="SamRidEnumeration"/> object identifying the user or group.
/// </param>
/// <returns>
/// One of the <see cref="PrincipalSource"/> enumerations identifying the
/// source of the object.
/// </returns>
private PrincipalSource? GetPrincipalSource(SamRidEnumeration sre)
{
return GetPrincipalSource(RidToSid(sre.domainHandle, sre.RelativeId));
}
#if CORECLR
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct OSVERSIONINFOEX
{
// The OSVersionInfoSize field must be set to Marshal.SizeOf(this)
public int OSVersionInfoSize;
public int MajorVersion;
public int MinorVersion;
public int BuildNumber;
public int PlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string CSDVersion;
public ushort ServicePackMajor;
public ushort ServicePackMinor;
public short SuiteMask;
public byte ProductType;
public byte Reserved;
}
[DllImport(PInvokeDllNames.GetVersionExDllName, CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool GetVersionEx(ref OSVERSIONINFOEX osVerEx);
private static volatile OperatingSystem localOs;
/// <summary>
/// It only contains the properties that get used in powershell.
/// </summary>
internal sealed class OperatingSystem
{
private Version _version;
private string _servicePack;
private string _versionString;
internal OperatingSystem(Version version, string servicePack)
{
if (version == null)
throw new ArgumentNullException("version");
_version = version;
_servicePack = servicePack;
}
/// <summary>
/// OS version.
/// </summary>
public Version Version
{
get { return _version; }
}
/// <summary>
/// VersionString.
/// </summary>
public string VersionString
{
get
{
if (_versionString != null)
{
return _versionString;
}
// It's always 'VER_PLATFORM_WIN32_NT' for NanoServer and IoT
const string os = "Microsoft Windows NT ";
if (string.IsNullOrEmpty(_servicePack))
{
_versionString = os + _version.ToString();
}
else
{
_versionString = os + _version.ToString(3) + " " + _servicePack;
}
return _versionString;
}
}
}
#endif
// Wraps calls to acquire the OperatingSystem version
private OperatingSystem GetOperatingSystem()
{
#if CORECLR
if (localOs == null)
{
OSVERSIONINFOEX osviex = new OSVERSIONINFOEX();
osviex.OSVersionInfoSize = Marshal.SizeOf(osviex);
if (!GetVersionEx(ref osviex))
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode);
}
Version ver = new Version(osviex.MajorVersion, osviex.MinorVersion, osviex.BuildNumber, (osviex.ServicePackMajor << 16) | osviex.ServicePackMinor);
localOs = new OperatingSystem(ver, osviex.CSDVersion);
}
return localOs;
#else
return Environment.OSVersion;
#endif
}
#endregion Utility Methods
#endregion Private Methods
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
UInt32 status = 0;
if (disposing)
{
// no managed objects need disposing.
}
if (builtinDomainHandle != IntPtr.Zero)
{
status = SamApi.SamCloseHandle(builtinDomainHandle);
builtinDomainHandle = IntPtr.Zero;
}
if (localDomainHandle != IntPtr.Zero)
{
status = SamApi.SamCloseHandle(localDomainHandle);
localDomainHandle = IntPtr.Zero;
}
if (samHandle != IntPtr.Zero)
{
status = SamApi.SamCloseHandle(samHandle);
samHandle = IntPtr.Zero;
}
if (NtStatus.IsError(status))
{
// Do nothing to satisfy CA1806: Do not ignore method results. We want the dispose to proceed regardless of the handle close status.
}
disposedValue = true;
}
}
// override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
~Sam()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(false);
}
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// uncomment the following line if the finalizer is overridden above.
GC.SuppressFinalize(this);
}
#endregion IDisposable Support
}
}