Private/AdminGroup-Functions.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Administrator group management functions for MakeMeAdminCLI. .DESCRIPTION Provides functions to add and remove users from the local Administrators group using the well-known SID S-1-5-32-544 for language independence. .NOTES Author: MakeMeAdminCLI Version: 1.0.0 The well-known SID S-1-5-32-544 identifies the built-in Administrators group on any Windows system, regardless of the display language. #> # Well-known SID for the local Administrators group $script:AdminGroupSID = "S-1-5-32-544" function Get-LocalAdministratorsGroupName { <# .SYNOPSIS Gets the localized name of the local Administrators group. .DESCRIPTION Uses the well-known SID S-1-5-32-544 to resolve the local Administrators group name. This works on any Windows language version. .OUTPUTS String containing the localized name of the Administrators group. .EXAMPLE $groupName = Get-LocalAdministratorsGroupName # Returns "Administrators" on English systems, "Administratoren" on German, etc. #> [CmdletBinding()] [OutputType([string])] param() try { $sid = New-Object System.Security.Principal.SecurityIdentifier($script:AdminGroupSID) $account = $sid.Translate([System.Security.Principal.NTAccount]) $fullName = $account.Value # Extract just the group name (remove BUILTIN\ or computer name prefix) if ($fullName -match '\\(.+)$') { return $Matches[1] } return $fullName } catch { throw "Failed to resolve Administrators group name from SID $script:AdminGroupSID : $($_.Exception.Message)" } } function Test-UserIsLocalAdmin { <# .SYNOPSIS Tests whether a user is currently a member of the local Administrators group. .DESCRIPTION Checks group membership using the localized Administrators group name resolved from the well-known SID. .PARAMETER Username The username to check, in DOMAIN\User or just User format. .OUTPUTS Boolean indicating whether the user is a member of the local Administrators group. .EXAMPLE Test-UserIsLocalAdmin -Username "DOMAIN\JohnDoe" Returns $true if the user is a local admin. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [string]$Username ) try { $groupName = Get-LocalAdministratorsGroupName $members = Get-LocalGroupMember -Group $groupName -ErrorAction SilentlyContinue foreach ($member in $members) { # Compare both the full name and just the username part if ($member.Name -eq $Username) { return $true } # Also check if just the username part matches (for local accounts) $memberUsername = $member.Name if ($memberUsername -match '\\(.+)$') { $memberUsername = $Matches[1] } $checkUsername = $Username if ($checkUsername -match '\\(.+)$') { $checkUsername = $Matches[1] } if ($memberUsername -eq $checkUsername) { return $true } } return $false } catch { Write-Warning "Error checking group membership for '$Username': $($_.Exception.Message)" return $false } } function Add-UserToLocalAdmins { <# .SYNOPSIS Adds a user to the local Administrators group. .DESCRIPTION Adds the specified user to the local Administrators group. Uses the well-known SID for language independence. The function first checks if the user is already a member to avoid errors. .PARAMETER Username The username to add, in DOMAIN\User format. .PARAMETER SkipMembershipCheck If specified, skips the check for existing membership. .OUTPUTS PSCustomObject with Success (bool), AlreadyMember (bool), and Message (string) properties. .EXAMPLE $result = Add-UserToLocalAdmins -Username "DOMAIN\JohnDoe" if ($result.Success) { Write-Host "User added successfully" } #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string]$Username, [switch]$SkipMembershipCheck ) $result = [PSCustomObject]@{ Success = $false AlreadyMember = $false Message = "" } try { $groupName = Get-LocalAdministratorsGroupName # Check if user is already a member if (-not $SkipMembershipCheck) { if (Test-UserIsLocalAdmin -Username $Username) { $result.AlreadyMember = $true $result.Success = $true $result.Message = "User '$Username' is already a member of the local Administrators group." Write-Verbose $result.Message return $result } } if ($PSCmdlet.ShouldProcess($Username, "Add to local Administrators group")) { # Try using Add-LocalGroupMember first (preferred method) try { Add-LocalGroupMember -Group $groupName -Member $Username -ErrorAction Stop $result.Success = $true $result.Message = "User '$Username' added to local Administrators group successfully." } catch { # Fallback to net localgroup command Write-Verbose "Add-LocalGroupMember failed, trying net localgroup: $($_.Exception.Message)" $netResult = & net localgroup "$groupName" "$Username" /add 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -eq 0) { $result.Success = $true $result.Message = "User '$Username' added to local Administrators group successfully (via net localgroup)." } elseif ($exitCode -eq 2 -or $netResult -match "already a member") { # Error code 2 typically means already a member $result.AlreadyMember = $true $result.Success = $true $result.Message = "User '$Username' is already a member of the local Administrators group." } else { throw "net localgroup returned exit code $exitCode : $netResult" } } # Verify the user was actually added if ($result.Success -and -not $result.AlreadyMember) { Start-Sleep -Milliseconds 500 # Brief pause for group membership to propagate if (-not (Test-UserIsLocalAdmin -Username $Username)) { Write-Warning "User '$Username' was added but membership could not be verified immediately." } } } else { $result.Message = "Operation cancelled by user (WhatIf mode)." } } catch { $result.Success = $false $result.Message = "Failed to add user '$Username' to local Administrators group: $($_.Exception.Message)" Write-Error $result.Message } return $result } function Remove-UserFromLocalAdmins { <# .SYNOPSIS Removes a user from the local Administrators group. .DESCRIPTION Removes the specified user from the local Administrators group. Uses the well-known SID for language independence. The function first checks if the user is actually a member. .PARAMETER Username The username to remove, in DOMAIN\User format. .PARAMETER SkipMembershipCheck If specified, skips the check for existing membership. .PARAMETER Force If specified, attempts removal even if the user doesn't appear to be a member. .OUTPUTS PSCustomObject with Success (bool), WasNotMember (bool), and Message (string) properties. .EXAMPLE $result = Remove-UserFromLocalAdmins -Username "DOMAIN\JohnDoe" if ($result.Success) { Write-Host "User removed successfully" } #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string]$Username, [switch]$SkipMembershipCheck, [switch]$Force ) $result = [PSCustomObject]@{ Success = $false WasNotMember = $false Message = "" } try { $groupName = Get-LocalAdministratorsGroupName # Check if user is actually a member if (-not $SkipMembershipCheck -and -not $Force) { if (-not (Test-UserIsLocalAdmin -Username $Username)) { $result.WasNotMember = $true $result.Success = $true $result.Message = "User '$Username' is not a member of the local Administrators group." Write-Verbose $result.Message return $result } } if ($PSCmdlet.ShouldProcess($Username, "Remove from local Administrators group")) { # Try using Remove-LocalGroupMember first (preferred method) try { Remove-LocalGroupMember -Group $groupName -Member $Username -ErrorAction Stop $result.Success = $true $result.Message = "User '$Username' removed from local Administrators group successfully." } catch { # Fallback to net localgroup command Write-Verbose "Remove-LocalGroupMember failed, trying net localgroup: $($_.Exception.Message)" $netResult = & net localgroup "$groupName" "$Username" /delete 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -eq 0) { $result.Success = $true $result.Message = "User '$Username' removed from local Administrators group successfully (via net localgroup)." } elseif ($netResult -match "not.+member" -or $netResult -match "does not belong") { $result.WasNotMember = $true $result.Success = $true $result.Message = "User '$Username' was not a member of the local Administrators group." } else { throw "net localgroup returned exit code $exitCode : $netResult" } } # Verify the user was actually removed if ($result.Success -and -not $result.WasNotMember) { Start-Sleep -Milliseconds 500 # Brief pause for group membership to propagate if (Test-UserIsLocalAdmin -Username $Username) { $result.Success = $false $result.Message = "User '$Username' still appears to be a member after removal attempt." Write-Warning $result.Message } } } else { $result.Message = "Operation cancelled by user (WhatIf mode)." } } catch { $result.Success = $false $result.Message = "Failed to remove user '$Username' from local Administrators group: $($_.Exception.Message)" Write-Error $result.Message } return $result } function Get-LocalAdminMembers { <# .SYNOPSIS Gets all members of the local Administrators group. .DESCRIPTION Returns a list of all users and groups that are members of the local Administrators group. .OUTPUTS Array of PSCustomObject with Name, SID, PrincipalSource, and ObjectClass properties. .EXAMPLE $admins = Get-LocalAdminMembers $admins | Format-Table Name, SID #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param() try { $groupName = Get-LocalAdministratorsGroupName $members = Get-LocalGroupMember -Group $groupName -ErrorAction Stop $result = @() foreach ($member in $members) { $result += [PSCustomObject]@{ Name = $member.Name SID = $member.SID.Value PrincipalSource = $member.PrincipalSource ObjectClass = $member.ObjectClass } } return $result } catch { Write-Error "Failed to get local Administrators group members: $($_.Exception.Message)" return @() } } function Get-UserSID { <# .SYNOPSIS Gets the Security Identifier (SID) for a username. .DESCRIPTION Resolves a username to its SID. .PARAMETER Username The username to resolve (DOMAIN\User or User format). .OUTPUTS String containing the SID, or $null if resolution fails. .EXAMPLE $sid = Get-UserSID -Username "DOMAIN\JohnDoe" #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string]$Username ) try { # Handle different username formats $resolvedUsername = $Username # If it starts with .\ replace with computer name if ($resolvedUsername.StartsWith(".\")) { $resolvedUsername = "$env:COMPUTERNAME\" + $resolvedUsername.Substring(2) } $account = New-Object System.Security.Principal.NTAccount($resolvedUsername) $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) return $sid.Value } catch { Write-Verbose "Failed to resolve SID for '$Username': $($_.Exception.Message)" return $null } } function Get-UsernameFromSID { <# .SYNOPSIS Gets the username for a Security Identifier (SID). .DESCRIPTION Resolves a SID to its username. .PARAMETER SID The SID string to resolve. .OUTPUTS String containing the username (DOMAIN\User format), or $null if resolution fails. .EXAMPLE $username = Get-UsernameFromSID -SID "S-1-5-21-..." #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string]$SID ) try { $sidObject = New-Object System.Security.Principal.SecurityIdentifier($SID) $account = $sidObject.Translate([System.Security.Principal.NTAccount]) return $account.Value } catch { Write-Verbose "Failed to resolve username for SID '$SID': $($_.Exception.Message)" return $null } } # Export module members (when dot-sourced from module) if ($MyInvocation.MyCommand.ScriptBlock.Module) { Export-ModuleMember -Function @( 'Get-LocalAdministratorsGroupName', 'Test-UserIsLocalAdmin', 'Add-UserToLocalAdmins', 'Remove-UserFromLocalAdmins', 'Get-LocalAdminMembers', 'Get-UserSID', 'Get-UsernameFromSID' ) Export-ModuleMember -Variable 'AdminGroupSID' } |