feat: integrate AD domain account impersonation for SQL Windows Authentication in SqlService, SettingsView, and SqlDatabaseView
This commit is contained in:
+210
-83
@@ -1,6 +1,99 @@
|
||||
# Modules\SqlService.ps1
|
||||
# Microsoft SQL Server Management Service Module (.NET System.Data.SqlClient Live & Fallback)
|
||||
|
||||
# P/Invoke helper for Windows Token Impersonation (AD Domain user outbound network authentication)
|
||||
$winAuthTypeDef = @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
public class WinAuthUniversalImpersonator {
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern bool LogonUser(
|
||||
string lpszUsername,
|
||||
string lpszDomain,
|
||||
string lpszPassword,
|
||||
int dwLogonType,
|
||||
int dwLogonProvider,
|
||||
out IntPtr phToken
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr handle);
|
||||
|
||||
public const int LOGON32_LOGON_NEW_CREDENTIALS = 9; // /netonly network outbound impersonation
|
||||
public const int LOGON32_PROVIDER_DEFAULT = 0;
|
||||
|
||||
public static void RunImpersonatedAction(string username, string domain, string password, Action action) {
|
||||
IntPtr tokenHandle = IntPtr.Zero;
|
||||
bool success = LogonUser(username, domain, password, LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_DEFAULT, out tokenHandle);
|
||||
if (!success || tokenHandle == IntPtr.Zero) {
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
throw new InvalidOperationException("AD 도메인 로그온(LogonUser) 실패: Win32 에러 코드 " + err);
|
||||
}
|
||||
|
||||
try {
|
||||
using (SafeAccessTokenHandle safeHandle = new SafeAccessTokenHandle(tokenHandle)) {
|
||||
WindowsIdentity.RunImpersonated(safeHandle, action);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// SafeAccessTokenHandle owns and closes tokenHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
if (-not ([System.Management.Automation.PSTypeName]"WinAuthUniversalImpersonator").Type) {
|
||||
Add-Type -TypeDefinition $winAuthTypeDef
|
||||
}
|
||||
|
||||
function Invoke-WithSqlAuthContext {
|
||||
param(
|
||||
[string]$AuthType = "Integrated",
|
||||
[string]$Domain = "",
|
||||
[string]$Username = "",
|
||||
[string]$Password = "",
|
||||
[scriptblock]$Action
|
||||
)
|
||||
|
||||
if ($AuthType -eq "AdImpersonated" -and -not [string]::IsNullOrWhiteSpace($Username) -and -not [string]::IsNullOrWhiteSpace($Password)) {
|
||||
$cleanDomain = $Domain
|
||||
$cleanUser = $Username
|
||||
|
||||
if ($cleanUser -like "*\*") {
|
||||
$parts = $cleanUser -split "\\", 2
|
||||
$cleanDomain = $parts[0]
|
||||
$cleanUser = $parts[1]
|
||||
} elseif ($cleanUser -like "*@*") {
|
||||
$parts = $cleanUser -split "@", 2
|
||||
$cleanUser = $parts[0]
|
||||
$cleanDomain = $parts[1]
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($cleanDomain)) {
|
||||
$cleanDomain = "."
|
||||
}
|
||||
|
||||
$resHolder = [hashtable]@{ Result = $null; Exception = $null }
|
||||
[WinAuthUniversalImpersonator]::RunImpersonatedAction($cleanUser, $cleanDomain, $Password, [Action]{
|
||||
try {
|
||||
$resHolder.Result = & $Action
|
||||
} catch {
|
||||
$resHolder.Exception = $_
|
||||
}
|
||||
})
|
||||
|
||||
if ($resHolder.Exception) {
|
||||
throw $resHolder.Exception
|
||||
}
|
||||
return $resHolder.Result
|
||||
} else {
|
||||
return & $Action
|
||||
}
|
||||
}
|
||||
|
||||
function Build-SqlConnectionString {
|
||||
param(
|
||||
[string]$Server,
|
||||
@@ -17,7 +110,8 @@ function Build-SqlConnectionString {
|
||||
$builder["Connect Timeout"] = $Timeout
|
||||
$builder["Application Name"] = "EnterpriseAdminStudio"
|
||||
|
||||
if ($AuthType -eq "Integrated" -or [string]::IsNullOrWhiteSpace($Username)) {
|
||||
# Integrated(현재 세션) 및 AdImpersonated(AD 위장 토큰)는 SQL Server 관점에서 둘 다 Windows 통합 인증(SSPI)
|
||||
if ($AuthType -eq "Integrated" -or $AuthType -eq "AdImpersonated" -or [string]::IsNullOrWhiteSpace($Username)) {
|
||||
$builder["Integrated Security"] = $true
|
||||
} else {
|
||||
$builder["Integrated Security"] = $false
|
||||
@@ -32,39 +126,54 @@ function Test-SqlConnection {
|
||||
param(
|
||||
[string]$Server,
|
||||
[string]$AuthType = "Integrated",
|
||||
[string]$Domain = "",
|
||||
[string]$Username = "",
|
||||
[string]$Password = "",
|
||||
[int]$Timeout = 5
|
||||
)
|
||||
|
||||
$connStr = Build-SqlConnectionString -Server $Server -AuthType $AuthType -Username $Username -Password $Password -Timeout $Timeout
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = "SELECT @@VERSION AS [Version], SERVERPROPERTY('ProductVersion') AS [ProductVersion], SERVERPROPERTY('Edition') AS [Edition]"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
if ($reader.Read()) {
|
||||
$ver = $reader["ProductVersion"].ToString()
|
||||
$edition = $reader["Edition"].ToString()
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "SQL Server 연결 성공 - 버전: $ver ($edition)"
|
||||
Version = "$ver ($edition)"
|
||||
$connStr = Build-SqlConnectionString -Server $Server -AuthType $AuthType -Username $Username -Password $Password -Timeout $Timeout
|
||||
|
||||
$execResult = Invoke-WithSqlAuthContext -AuthType $AuthType -Domain $Domain -Username $Username -Password $Password -Action {
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = "SELECT SUSER_SNAME() AS [LoginName], @@VERSION AS [Version], SERVERPROPERTY('ProductVersion') AS [ProductVersion], SERVERPROPERTY('Edition') AS [Edition]"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
if ($reader.Read()) {
|
||||
$loginName = $reader["LoginName"].ToString()
|
||||
$ver = $reader["ProductVersion"].ToString()
|
||||
$edition = $reader["Edition"].ToString()
|
||||
|
||||
$authDesc = switch ($AuthType) {
|
||||
"AdImpersonated" { "AD 계정 위장 Windows 통합 인증 ($loginName)" }
|
||||
"Integrated" { "현재 세션 Windows 통합 인증 ($loginName)" }
|
||||
default { "SQL 계정 인증 ($loginName)" }
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "SQL Server 연결 성공!`n- 인증 방식: $authDesc`n- 서버 버전: $ver ($edition)"
|
||||
LoginName = $loginName
|
||||
Version = "$ver ($edition)"
|
||||
}
|
||||
}
|
||||
return [PSCustomObject]@{ Success = $true; Message = "SQL Server 연결 성공" }
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) {
|
||||
$conn.Close()
|
||||
}
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
return [PSCustomObject]@{ Success = $true; Message = "SQL Server 연결 성공" }
|
||||
return $execResult
|
||||
} catch {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Message = "SQL Server 연결 실패: $($_.Exception.Message)"
|
||||
}
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) {
|
||||
$conn.Close()
|
||||
}
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,17 +188,20 @@ function Get-SqlDatabases {
|
||||
if ($useLive) {
|
||||
$server = $Config.MsSql.Server
|
||||
$auth = $Config.MsSql.AuthType
|
||||
$domain = if ($Config.MsSql.Domain) { $Config.MsSql.Domain } else { "" }
|
||||
$user = $Config.MsSql.Username
|
||||
$pass = $Config.MsSql.Password
|
||||
$timeout = if ($Config.MsSql.ConnectionTimeout) { [int]$Config.MsSql.ConnectionTimeout } else { 3 }
|
||||
|
||||
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout $timeout
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = @"
|
||||
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout $timeout
|
||||
|
||||
$dbList = Invoke-WithSqlAuthContext -AuthType $auth -Domain $domain -Username $user -Password $pass -Action {
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = @"
|
||||
SELECT
|
||||
d.name AS DatabaseName,
|
||||
d.state_desc AS StateDesc,
|
||||
@@ -103,32 +215,35 @@ LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
|
||||
GROUP BY d.name, d.state_desc, d.recovery_model_desc, d.collation_name, b.type
|
||||
ORDER BY d.name;
|
||||
"@
|
||||
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
|
||||
$table = New-Object System.Data.DataTable
|
||||
$adapter.Fill($table) | Out-Null
|
||||
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
|
||||
$table = New-Object System.Data.DataTable
|
||||
$adapter.Fill($table) | Out-Null
|
||||
|
||||
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
foreach ($row in $table.Rows) {
|
||||
$dbName = $row["DatabaseName"]
|
||||
if ([string]::IsNullOrWhiteSpace($Filter) -or $dbName -like "*$Filter*") {
|
||||
$list.Add([PSCustomObject]@{
|
||||
DatabaseName = $dbName
|
||||
StateDesc = $row["StateDesc"]
|
||||
TotalSizeMB = $row["TotalSizeMB"]
|
||||
FreeSpaceMB = "실시간 계산"
|
||||
RecoveryModel = $row["RecoveryModel"]
|
||||
CollationName = $row["CollationName"]
|
||||
LastBackupDate = $row["LastBackupDate"]
|
||||
IsLive = $true
|
||||
})
|
||||
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
foreach ($row in $table.Rows) {
|
||||
$dbName = $row["DatabaseName"]
|
||||
if ([string]::IsNullOrWhiteSpace($Filter) -or $dbName -like "*$Filter*") {
|
||||
$list.Add([PSCustomObject]@{
|
||||
DatabaseName = $dbName
|
||||
StateDesc = $row["StateDesc"]
|
||||
TotalSizeMB = $row["TotalSizeMB"]
|
||||
FreeSpaceMB = "실시간 계산"
|
||||
RecoveryModel = $row["RecoveryModel"]
|
||||
CollationName = $row["CollationName"]
|
||||
LastBackupDate = $row["LastBackupDate"]
|
||||
IsLive = $true
|
||||
})
|
||||
}
|
||||
}
|
||||
return $list.ToArray()
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
return $list.ToArray()
|
||||
return $dbList
|
||||
} catch {
|
||||
Write-Warning "실제 SQL Server 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +262,7 @@ function Invoke-SqlBackupAction {
|
||||
if ($useLive) {
|
||||
$server = $Config.MsSql.Server
|
||||
$auth = $Config.MsSql.AuthType
|
||||
$domain = if ($Config.MsSql.Domain) { $Config.MsSql.Domain } else { "" }
|
||||
$user = $Config.MsSql.Username
|
||||
$pass = $Config.MsSql.Password
|
||||
$backupDir = if ($Config.MsSql.BackupDirectory) { $Config.MsSql.BackupDirectory } else { "C:\Backups" }
|
||||
@@ -159,31 +275,36 @@ function Invoke-SqlBackupAction {
|
||||
$fileName = "$DatabaseName`_$timeStamp.bak"
|
||||
$targetPath = Join-Path $backupDir $fileName
|
||||
|
||||
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 30
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandTimeout = 300 # 백업은 5분 타임아웃
|
||||
$cmd.CommandText = "BACKUP DATABASE [$DatabaseName] TO DISK = N'$targetPath' WITH INIT, STATS = 10, NAME = N'$DatabaseName-Full Backup'"
|
||||
$cmd.ExecuteNonQuery() | Out-Null
|
||||
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 30
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Database = $DatabaseName
|
||||
BackupPath = $targetPath
|
||||
Message = "실제 SQL Server 데이터베이스 [$DatabaseName] 백업이 완료되었습니다.`n저장 경로: $targetPath"
|
||||
$res = Invoke-WithSqlAuthContext -AuthType $auth -Domain $domain -Username $user -Password $pass -Action {
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandTimeout = 300 # 백업은 5분 타임아웃
|
||||
$cmd.CommandText = "BACKUP DATABASE [$DatabaseName] TO DISK = N'$targetPath' WITH INIT, STATS = 10, NAME = N'$DatabaseName-Full Backup'"
|
||||
$cmd.ExecuteNonQuery() | Out-Null
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Database = $DatabaseName
|
||||
BackupPath = $targetPath
|
||||
Message = "실제 SQL Server 데이터베이스 [$DatabaseName] 백업이 완료되었습니다.`n저장 경로: $targetPath"
|
||||
}
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
return $res
|
||||
} catch {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Database = $DatabaseName
|
||||
Message = "SQL 백업 실패: $($_.Exception.Message)"
|
||||
}
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,37 +327,43 @@ function Invoke-SqlQuerySnippet {
|
||||
if ($useLive) {
|
||||
$server = $Config.MsSql.Server
|
||||
$auth = $Config.MsSql.AuthType
|
||||
$domain = if ($Config.MsSql.Domain) { $Config.MsSql.Domain } else { "" }
|
||||
$user = $Config.MsSql.Username
|
||||
$pass = $Config.MsSql.Password
|
||||
|
||||
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 15
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = $Query
|
||||
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
|
||||
$table = New-Object System.Data.DataTable
|
||||
$adapter.Fill($table) | Out-Null
|
||||
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 15
|
||||
|
||||
$sw = [System.IO.StringWriter]::new()
|
||||
$table | Format-Table -AutoSize | Out-String | ForEach-Object { $sw.WriteLine($_) }
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
RowCount = $table.Rows.Count
|
||||
Output = "(성공: $($table.Rows.Count)개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n$($sw.ToString())"
|
||||
$res = Invoke-WithSqlAuthContext -AuthType $auth -Domain $domain -Username $user -Password $pass -Action {
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
|
||||
try {
|
||||
$conn.Open()
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = $Query
|
||||
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
|
||||
$table = New-Object System.Data.DataTable
|
||||
$adapter.Fill($table) | Out-Null
|
||||
|
||||
$sw = [System.IO.StringWriter]::new()
|
||||
$table | Format-Table -AutoSize | Out-String | ForEach-Object { $sw.WriteLine($_) }
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
RowCount = $table.Rows.Count
|
||||
Output = "(성공: $($table.Rows.Count)개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n$($sw.ToString())"
|
||||
}
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
return $res
|
||||
} catch {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
RowCount = 0
|
||||
Output = "(오류 발생)`n$($_.Exception.Message)"
|
||||
}
|
||||
} finally {
|
||||
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
|
||||
$conn.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user