docs+tools: UG_WW_* AD group -> app role mapping proposal (ldap_mapping.md); tools/sql/Ensure-SysadminLogin (create/repair a SQL login with a parameter-bound password, optional sysadmin grant)
Claude-Session: https://claude.ai/code/session_013L3XdmVii9i5sY4FChU8pB
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates or repairs a SQL Server login and ensures it is a member of the
|
||||
sysadmin fixed server role -- the thing usually meant by "the SA group".
|
||||
|
||||
.DESCRIPTION
|
||||
A thin, secure runner for Ensure-SysadminLogin.sql. It reads that file,
|
||||
discards everything above the --#PARAMS-END sentinel, and executes the
|
||||
remainder through sp_executesql with real SqlParameters.
|
||||
|
||||
That indirection is the point: the password is bound as a parameter, so it
|
||||
never appears in the command line (visible to any user via the process
|
||||
list), in shell history, in a temp file, or in the SQL text captured by
|
||||
Profiler / Extended Events / the query store.
|
||||
|
||||
Everything the script actually does -- and why -- is documented in the .sql
|
||||
file. This wrapper adds no logic of its own.
|
||||
|
||||
.PARAMETER ServerInstance
|
||||
Target instance, e.g. "wonder-sql-vd03" or "HOST\INSTANCE,1433".
|
||||
|
||||
.PARAMETER LoginName
|
||||
SQL Authentication login to create or repair.
|
||||
|
||||
.PARAMETER Password
|
||||
Password as a SecureString. Prompted for (never echoed) when omitted.
|
||||
|
||||
.PARAMETER AdminCredential
|
||||
Credential to CONNECT with, if not using Windows Integrated auth. Must
|
||||
already be a sysadmin -- granting sysadmin requires holding it.
|
||||
|
||||
.PARAMETER NoSysadmin
|
||||
Create/repair the login and set its password, but do NOT grant sysadmin.
|
||||
|
||||
.PARAMETER DisablePasswordPolicy
|
||||
Sets CHECK_POLICY = OFF. Only for a service account that a domain password
|
||||
policy would otherwise reject. Note this also forces CHECK_EXPIRATION off,
|
||||
and toggling the flag back on later clears the password history.
|
||||
|
||||
.PARAMETER TrustServerCertificate
|
||||
Skip TLS validation of the SQL Server certificate. Required against an
|
||||
instance using a self-signed cert, which is the norm on the internal boxes
|
||||
here. It is NOT the default: this connection carries a sysadmin password,
|
||||
so accepting any certificate makes it interceptable. Prefer installing the
|
||||
issuing CA into the client trust store.
|
||||
|
||||
.EXAMPLE
|
||||
./Ensure-SysadminLogin.ps1 -ServerInstance wonder-sql-vd03 -LoginName svc_example -TrustServerCertificate
|
||||
|
||||
.EXAMPLE
|
||||
$c = Get-Credential sa
|
||||
./Ensure-SysadminLogin.ps1 -ServerInstance localhost,1433 -LoginName appuser -AdminCredential $c
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $ServerInstance,
|
||||
[Parameter(Mandatory = $true)][string] $LoginName,
|
||||
[Parameter()][securestring] $Password,
|
||||
[Parameter()][pscredential] $AdminCredential,
|
||||
[Parameter()][string] $DefaultDatabase = 'master',
|
||||
[Parameter()][switch] $NoSysadmin,
|
||||
[Parameter()][switch] $DisablePasswordPolicy,
|
||||
[Parameter()][switch] $TrustServerCertificate,
|
||||
[Parameter()][int] $ConnectTimeoutSeconds = 15,
|
||||
[Parameter()][string] $SqlFile
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Locate the .sql and take only the part below the sentinel. The parameter
|
||||
# DECLAREs above it exist for standalone SSMS use; keeping them here would
|
||||
# collide with the sp_executesql parameter list.
|
||||
# ---------------------------------------------------------------------------
|
||||
if (-not $SqlFile) {
|
||||
$SqlFile = Join-Path -Path $PSScriptRoot -ChildPath 'Ensure-SysadminLogin.sql'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $SqlFile)) {
|
||||
throw "Cannot find the SQL script at '$SqlFile'. Pass -SqlFile to point at it."
|
||||
}
|
||||
|
||||
# Matched as a whole line, not as a substring: the sentinel is also named in the
|
||||
# .sql header prose, and a substring search finds that mention first -- which
|
||||
# leaves the parameter DECLAREs in the body and collides with the sp_executesql
|
||||
# parameter list. Requiring exactly one whole-line match makes that failure
|
||||
# impossible rather than merely unlikely.
|
||||
$sentinel = '--#PARAMS-END'
|
||||
$lines = @(Get-Content -LiteralPath $SqlFile)
|
||||
$hits = @(0..($lines.Count - 1) | Where-Object { $lines[$_].Trim() -ceq $sentinel })
|
||||
|
||||
if ($hits.Count -eq 0) {
|
||||
throw "'$SqlFile' has no line consisting solely of $sentinel. It has been edited in a way this wrapper cannot parse; run the .sql directly in SSMS instead."
|
||||
}
|
||||
if ($hits.Count -gt 1) {
|
||||
throw "'$SqlFile' has $($hits.Count) $sentinel lines (at $($hits -join ', ')). Exactly one is required, or the wrapper cannot tell where the parameters end."
|
||||
}
|
||||
$body = ($lines[($hits[0] + 1)..($lines.Count - 1)]) -join [Environment]::NewLine
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resolve an ADO.NET client. Microsoft.Data.SqlClient arrives with the SqlServer
|
||||
# module; System.Data.SqlClient is in the box on Windows PowerShell 5.1. .NET
|
||||
# Core ships neither, hence the explicit failure rather than a confusing
|
||||
# "type not found" further down.
|
||||
# ---------------------------------------------------------------------------
|
||||
function Resolve-SqlClientNamespace {
|
||||
foreach ($ns in 'Microsoft.Data.SqlClient', 'System.Data.SqlClient') {
|
||||
if ([Type]::GetType("$ns.SqlConnection, $ns", $false)) { return $ns }
|
||||
if ("$ns.SqlConnection" -as [Type]) { return $ns }
|
||||
}
|
||||
try {
|
||||
Import-Module SqlServer -ErrorAction Stop | Out-Null
|
||||
if ('Microsoft.Data.SqlClient.SqlConnection' -as [Type]) { return 'Microsoft.Data.SqlClient' }
|
||||
} catch { }
|
||||
try {
|
||||
Add-Type -AssemblyName System.Data -ErrorAction Stop
|
||||
if ('System.Data.SqlClient.SqlConnection' -as [Type]) { return 'System.Data.SqlClient' }
|
||||
} catch { }
|
||||
throw 'No SQL Server ADO.NET client is available. Install the SqlServer PowerShell module (Install-Module SqlServer), or run the .sql file directly in SSMS / sqlcmd.'
|
||||
}
|
||||
|
||||
$ns = Resolve-SqlClientNamespace
|
||||
Write-Verbose "Using $ns"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prompt for the new password if it was not supplied, and confirm it. A typo in
|
||||
# a password that is simultaneously being set and never displayed is otherwise
|
||||
# undiscoverable until the first failed login.
|
||||
# ---------------------------------------------------------------------------
|
||||
if (-not $Password) {
|
||||
$Password = Read-Host -AsSecureString -Prompt "New password for SQL login '$LoginName'"
|
||||
$confirm = Read-Host -AsSecureString -Prompt 'Confirm password'
|
||||
|
||||
$a = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
|
||||
$b = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($confirm)
|
||||
try {
|
||||
if ([Runtime.InteropServices.Marshal]::PtrToStringBSTR($a) -cne
|
||||
[Runtime.InteropServices.Marshal]::PtrToStringBSTR($b)) {
|
||||
throw 'Passwords do not match.'
|
||||
}
|
||||
} finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($a)
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($b)
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Connection string.
|
||||
# ---------------------------------------------------------------------------
|
||||
$csb = New-Object "$ns.SqlConnectionStringBuilder"
|
||||
$csb['Data Source'] = $ServerInstance
|
||||
$csb['Initial Catalog'] = 'master'
|
||||
$csb['Connect Timeout'] = $ConnectTimeoutSeconds
|
||||
$csb['Application Name'] = 'Ensure-SysadminLogin'
|
||||
|
||||
if ($AdminCredential) {
|
||||
$csb['User ID'] = $AdminCredential.UserName
|
||||
$csb['Password'] = $AdminCredential.GetNetworkCredential().Password
|
||||
} else {
|
||||
$csb['Integrated Security'] = $true
|
||||
}
|
||||
|
||||
if ($TrustServerCertificate) {
|
||||
$csb['TrustServerCertificate'] = $true
|
||||
Write-Warning 'TLS certificate validation is DISABLED for this connection (-TrustServerCertificate). A sysadmin password is being sent over it. Use this only on a trusted network segment.'
|
||||
}
|
||||
|
||||
$target = "login '$LoginName' on '$ServerInstance'"
|
||||
$action = if ($NoSysadmin) { 'create/repair (no sysadmin grant)' } else { 'create/repair and grant sysadmin' }
|
||||
if (-not $PSCmdlet.ShouldProcess($target, $action)) { return }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Execute. The body runs inside sp_executesql with the parameters it expects.
|
||||
# ---------------------------------------------------------------------------
|
||||
$outer = @'
|
||||
EXEC sys.sp_executesql
|
||||
@stmt = @Body,
|
||||
@params = N'@LoginName sysname, @Password nvarchar(128), @CheckPolicy bit, @GrantSysadmin bit, @DefaultDatabase sysname',
|
||||
@LoginName = @P_LoginName,
|
||||
@Password = @P_Password,
|
||||
@CheckPolicy = @P_CheckPolicy,
|
||||
@GrantSysadmin = @P_GrantSysadmin,
|
||||
@DefaultDatabase = @P_DefaultDatabase;
|
||||
'@
|
||||
|
||||
$conn = New-Object "$ns.SqlConnection" $csb.ConnectionString
|
||||
$plainPtr = [IntPtr]::Zero
|
||||
try {
|
||||
# RAISERROR ... WITH NOWAIT from the script arrives as an info message, not
|
||||
# as output. Without this handler the run looks silent.
|
||||
$conn.add_InfoMessage({ param($s, $e) foreach ($err in $e.Errors) { Write-Host " $($err.Message)" } })
|
||||
$conn.Open()
|
||||
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = $outer
|
||||
$cmd.CommandTimeout = 60
|
||||
|
||||
$plainPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
|
||||
$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($plainPtr)
|
||||
|
||||
[void]$cmd.Parameters.AddWithValue('@Body', $body)
|
||||
[void]$cmd.Parameters.AddWithValue('@P_LoginName', $LoginName)
|
||||
[void]$cmd.Parameters.AddWithValue('@P_Password', $plain)
|
||||
[void]$cmd.Parameters.AddWithValue('@P_CheckPolicy', -not $DisablePasswordPolicy)
|
||||
[void]$cmd.Parameters.AddWithValue('@P_GrantSysadmin', -not $NoSysadmin)
|
||||
[void]$cmd.Parameters.AddWithValue('@P_DefaultDatabase', $DefaultDatabase)
|
||||
|
||||
$reader = $cmd.ExecuteReader()
|
||||
try {
|
||||
$rows = @()
|
||||
while ($reader.Read()) {
|
||||
$o = [ordered]@{}
|
||||
for ($i = 0; $i -lt $reader.FieldCount; $i++) {
|
||||
$o[$reader.GetName($i)] = $reader.GetValue($i)
|
||||
}
|
||||
$rows += [pscustomobject]$o
|
||||
}
|
||||
# Emitted as objects, so the caller can assert on them rather than
|
||||
# scraping formatted text.
|
||||
$rows
|
||||
} finally {
|
||||
$reader.Dispose()
|
||||
}
|
||||
} finally {
|
||||
if ($plainPtr -ne [IntPtr]::Zero) {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($plainPtr)
|
||||
}
|
||||
$conn.Dispose()
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*==============================================================================
|
||||
Ensure-SysadminLogin.sql
|
||||
----------------------------------------------------------------------------
|
||||
Idempotently ensures a SQL Server *login* exists, has a known password, is
|
||||
enabled and unlocked, and is a member of the sysadmin fixed server role.
|
||||
|
||||
Target: SQL Server 2022 (16.x). Also correct on 2012+; the only 2022-specific
|
||||
content is the granular-role note at the end of the file.
|
||||
|
||||
ON THE NAME "SA GROUP"
|
||||
----------------------
|
||||
SQL Server has no group called "SA". `sa` is a single built-in *login*. The
|
||||
privilege people mean by "sa rights" comes from the **sysadmin fixed server
|
||||
role**, of which `sa` is merely one member. This script therefore adds the
|
||||
login to `sysadmin`.
|
||||
|
||||
* sysadmin bypasses ALL permission checks. Members can do anything on the
|
||||
instance, including dropping databases and reading every credential the
|
||||
service account can decrypt. Grant it deliberately.
|
||||
* ALTER SERVER ROLE is the supported syntax. sp_addsrvrolemember is
|
||||
deprecated and is not used here.
|
||||
* SQL Server 2022 adds granular fixed server roles that cover most reasons
|
||||
people reach for sysadmin. See the note at the end of this file.
|
||||
|
||||
USAGE
|
||||
-----
|
||||
SSMS / Azure Data Studio / sqlcmd -i : edit the PARAMETERS block below and run.
|
||||
Interactively with no password on disk or in shell history:
|
||||
pwsh ./Ensure-SysadminLogin.ps1 -ServerInstance wonder-sql-vd03 -LoginName svc_example
|
||||
|
||||
The .ps1 wrapper discards everything above the --#PARAMS-END sentinel and runs
|
||||
the remainder through sp_executesql with real SqlParameters, so the password
|
||||
never appears in the command line, the SQL text, a temp file, or a trace.
|
||||
Do not remove that sentinel, and do not declare the parameter variables below
|
||||
it -- either change silently breaks the wrapper.
|
||||
|
||||
REQUIRES
|
||||
--------
|
||||
A caller holding ALTER ANY LOGIN and CONTROL SERVER (in practice, an existing
|
||||
sysadmin). Anything less cannot grant sysadmin membership.
|
||||
|
||||
SAFETY
|
||||
------
|
||||
* Wrapped in a transaction with XACT_ABORT; rolls back on any error.
|
||||
* Refuses to touch a principal that is not a SQL login. A Windows login or
|
||||
group of the same name has no password to set, and silently skipping that
|
||||
would be worse than failing.
|
||||
* The password is escaped via QUOTENAME rather than concatenated raw, and is
|
||||
never printed by this script.
|
||||
==============================================================================*/
|
||||
|
||||
/*------------------------------------------------------------------------------
|
||||
PARAMETERS -- edit these two (three if the password policy is in the way).
|
||||
|
||||
A single quote in the password must be DOUBLED here, because you are editing a
|
||||
T-SQL string literal: pa'ss -> N'pa''ss'
|
||||
Get that wrong and the literal ends early: the password silently becomes the
|
||||
truncated prefix, and the remainder of your text is parsed as SQL and executed.
|
||||
The .ps1 wrapper binds the password as a parameter and so has no such rule --
|
||||
another reason to prefer it for anything but a quick manual run.
|
||||
------------------------------------------------------------------------------*/
|
||||
DECLARE @LoginName sysname = N'wwadmin';
|
||||
DECLARE @Password nvarchar(128) = N'SomeStr0ng!Passw0rd';
|
||||
|
||||
/* CHECK_POLICY = 1 enforces the Windows password policy (complexity, history,
|
||||
lockout) against this login. Leave it on unless a policy you do not control
|
||||
is blocking a service account. Turning it OFF also forces CHECK_EXPIRATION
|
||||
off, and toggling it OFF then ON later clears the password history. */
|
||||
DECLARE @CheckPolicy bit = 1;
|
||||
|
||||
/* Set to 0 to create/fix the login but NOT grant sysadmin -- useful when only
|
||||
the password-reset half is wanted. */
|
||||
DECLARE @GrantSysadmin bit = 1;
|
||||
|
||||
DECLARE @DefaultDatabase sysname = N'master';
|
||||
|
||||
--#PARAMS-END
|
||||
/*==============================================================================
|
||||
No editing required below this line.
|
||||
==============================================================================*/
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
DECLARE @sql nvarchar(max);
|
||||
DECLARE @msg nvarchar(400);
|
||||
DECLARE @type char(1);
|
||||
DECLARE @isDisabled bit;
|
||||
DECLARE @created bit = 0;
|
||||
DECLARE @roleAdded bit = 0;
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 0. Validate inputs before touching anything.
|
||||
------------------------------------------------------------------------------
|
||||
IF @LoginName IS NULL OR LTRIM(RTRIM(@LoginName)) = N'' OR @LoginName = N'CHANGE_ME'
|
||||
THROW 50001, 'LoginName was not supplied. Edit the PARAMETERS block, or use the .ps1 wrapper.', 1;
|
||||
|
||||
IF @Password IS NULL OR @Password = N'' OR @Password = N'CHANGE_ME'
|
||||
THROW 50002, 'Password was not supplied. Edit the PARAMETERS block, or use the .ps1 wrapper.', 1;
|
||||
|
||||
-- A backslash means a Windows principal (DOMAIN\user). Those have no SQL
|
||||
-- password, so the caller has almost certainly confused the two login types.
|
||||
IF CHARINDEX(N'\', @LoginName) > 0
|
||||
THROW 50003, 'LoginName looks like a Windows principal (contains a backslash). This script manages SQL Authentication logins only.', 1;
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 1. Can the instance even serve a SQL login?
|
||||
-- IsIntegratedSecurityOnly = 1 means Windows Authentication mode. The login
|
||||
-- can still be created, but nobody will ever connect with it -- so say so
|
||||
-- rather than reporting a success the caller cannot use.
|
||||
------------------------------------------------------------------------------
|
||||
IF CONVERT(int, SERVERPROPERTY('IsIntegratedSecurityOnly')) = 1
|
||||
RAISERROR('WARNING: instance is in Windows Authentication mode. The SQL login will be created but CANNOT be used to connect until Mixed Mode is enabled and the instance is restarted.', 10, 1) WITH NOWAIT;
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 2. Inspect any existing principal of this name.
|
||||
------------------------------------------------------------------------------
|
||||
SELECT @type = sp.type,
|
||||
@isDisabled = sp.is_disabled
|
||||
FROM sys.server_principals AS sp
|
||||
WHERE sp.name = @LoginName;
|
||||
|
||||
IF @type IS NOT NULL AND @type <> 'S'
|
||||
BEGIN
|
||||
SET @msg = FORMATMESSAGE(
|
||||
'A server principal named "%s" already exists but is of type "%s" (S=SQL login, U=Windows login, G=Windows group, R=server role, C=certificate-mapped, K=asymmetric-key-mapped). Refusing to proceed: only a SQL login has a password this script can set.',
|
||||
@LoginName, @type);
|
||||
THROW 50004, @msg, 1;
|
||||
END
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 3. Create, or reset the password.
|
||||
-- QUOTENAME with a doubled-quote delimiter is what makes the password safe
|
||||
-- to embed: a password containing a single quote would otherwise terminate
|
||||
-- the literal and inject SQL.
|
||||
------------------------------------------------------------------------------
|
||||
IF @type IS NULL
|
||||
BEGIN
|
||||
SET @sql = N'CREATE LOGIN ' + QUOTENAME(@LoginName)
|
||||
+ N' WITH PASSWORD = ' + QUOTENAME(@Password, '''')
|
||||
+ N', DEFAULT_DATABASE = ' + QUOTENAME(@DefaultDatabase)
|
||||
+ N', CHECK_POLICY = ' + CASE WHEN @CheckPolicy = 1 THEN N'ON' ELSE N'OFF' END
|
||||
+ CASE WHEN @CheckPolicy = 1 THEN N', CHECK_EXPIRATION = OFF' ELSE N'' END
|
||||
+ N';';
|
||||
EXEC sys.sp_executesql @sql;
|
||||
SET @created = 1;
|
||||
RAISERROR('Created SQL login.', 10, 1) WITH NOWAIT;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
-- Policy first: a password that satisfies the new policy may violate the
|
||||
-- old one, so the flag has to move before the password does.
|
||||
SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName)
|
||||
+ N' WITH CHECK_POLICY = ' + CASE WHEN @CheckPolicy = 1 THEN N'ON' ELSE N'OFF' END + N';';
|
||||
EXEC sys.sp_executesql @sql;
|
||||
|
||||
-- UNLOCK clears an account locked out by failed logins. It is only legal
|
||||
-- alongside a password change, which is exactly what is happening here,
|
||||
-- and only meaningful while CHECK_POLICY is ON.
|
||||
SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName)
|
||||
+ N' WITH PASSWORD = ' + QUOTENAME(@Password, '''')
|
||||
+ CASE WHEN @CheckPolicy = 1 THEN N' UNLOCK' ELSE N'' END
|
||||
+ N';';
|
||||
EXEC sys.sp_executesql @sql;
|
||||
|
||||
SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName)
|
||||
+ N' WITH DEFAULT_DATABASE = ' + QUOTENAME(@DefaultDatabase) + N';';
|
||||
EXEC sys.sp_executesql @sql;
|
||||
|
||||
RAISERROR('Login already existed; password reset and unlocked.', 10, 1) WITH NOWAIT;
|
||||
END
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 4. Enable it. A disabled login will not connect no matter how good its
|
||||
-- password or how high its role membership.
|
||||
------------------------------------------------------------------------------
|
||||
IF @created = 0 AND @isDisabled = 1
|
||||
BEGIN
|
||||
SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName) + N' ENABLE;';
|
||||
EXEC sys.sp_executesql @sql;
|
||||
RAISERROR('Login was disabled; re-enabled.', 10, 1) WITH NOWAIT;
|
||||
END
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 5. sysadmin membership -- the thing "SA group" actually means.
|
||||
-- ADD MEMBER is already idempotent; the explicit check exists so the summary
|
||||
-- below can distinguish "granted now" from "already had it".
|
||||
------------------------------------------------------------------------------
|
||||
IF @GrantSysadmin = 1
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.server_role_members AS srm
|
||||
JOIN sys.server_principals AS r ON r.principal_id = srm.role_principal_id
|
||||
JOIN sys.server_principals AS m ON m.principal_id = srm.member_principal_id
|
||||
WHERE r.name = N'sysadmin'
|
||||
AND m.name = @LoginName)
|
||||
BEGIN
|
||||
SET @sql = N'ALTER SERVER ROLE [sysadmin] ADD MEMBER ' + QUOTENAME(@LoginName) + N';';
|
||||
EXEC sys.sp_executesql @sql;
|
||||
SET @roleAdded = 1;
|
||||
RAISERROR('Added to the sysadmin fixed server role.', 10, 1) WITH NOWAIT;
|
||||
END
|
||||
ELSE
|
||||
RAISERROR('Already a member of sysadmin; no change.', 10, 1) WITH NOWAIT;
|
||||
END
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 6. Verify from the catalog, not from what the script believes it just did.
|
||||
------------------------------------------------------------------------------
|
||||
SELECT LoginName = sp.name,
|
||||
LoginType = sp.type_desc,
|
||||
Enabled = CASE WHEN sp.is_disabled = 1 THEN 'NO' ELSE 'YES' END,
|
||||
IsSysadmin = CASE WHEN IS_SRVROLEMEMBER(N'sysadmin', sp.name) = 1 THEN 'YES' ELSE 'NO' END,
|
||||
PolicyChecked = CASE WHEN sl.is_policy_checked = 1 THEN 'YES' ELSE 'NO' END,
|
||||
ExpirationChecked = CASE WHEN sl.is_expiration_checked = 1 THEN 'YES' ELSE 'NO' END,
|
||||
LockedOut = CASE WHEN LOGINPROPERTY(sp.name, 'IsLocked') = 1 THEN 'YES' ELSE 'NO' END,
|
||||
MustChange = CASE WHEN LOGINPROPERTY(sp.name, 'IsMustChange') = 1 THEN 'YES' ELSE 'NO' END,
|
||||
DefaultDatabase = sp.default_database_name,
|
||||
PasswordLastSet = LOGINPROPERTY(sp.name, 'PasswordLastSetTime'),
|
||||
Action = CASE WHEN @created = 1 THEN 'CREATED' ELSE 'UPDATED' END
|
||||
+ CASE WHEN @roleAdded = 1 THEN ' +SYSADMIN' ELSE '' END
|
||||
FROM sys.server_principals AS sp
|
||||
LEFT JOIN sys.sql_logins AS sl ON sl.principal_id = sp.principal_id
|
||||
WHERE sp.name = @LoginName;
|
||||
|
||||
/*==============================================================================
|
||||
IS SYSADMIN ACTUALLY WHAT YOU WANT?
|
||||
|
||||
On SQL Server 2022 these fixed server roles cover most reasons people reach
|
||||
for sysadmin, without granting the ability to destroy the instance:
|
||||
|
||||
##MS_ServerStateReader## read DMVs / performance state
|
||||
##MS_ServerPerformanceStateReader## the performance subset of the above
|
||||
##MS_DatabaseConnector## CONNECT to every database
|
||||
##MS_DefinitionReader## read metadata / object definitions
|
||||
##MS_DatabaseManager## create and drop databases
|
||||
##MS_LoginManager## create and drop logins
|
||||
##MS_SecurityDefinitionReader## read security metadata
|
||||
|
||||
Same syntax: ALTER SERVER ROLE [##MS_DatabaseConnector##] ADD MEMBER [x];
|
||||
|
||||
A monitoring, reporting, or read-only application account almost never needs
|
||||
sysadmin. An application that owns exactly one database wants db_owner in that
|
||||
database instead -- a server-level role is the wrong tool for it.
|
||||
==============================================================================*/
|
||||
Reference in New Issue
Block a user