feat: integrate AD domain account impersonation for SQL Windows Authentication in SqlService, SettingsView, and SqlDatabaseView
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
# Enterprise Admin Studio - 변경 이력 (CHANGELOG.md)
|
||||
|
||||
## [v1.6.0] - 2026-10-21
|
||||
### Added
|
||||
- **MS SQL Server AD 도메인 계정 기반 Windows 통합 인증 위장(Impersonation) 엔진 탑재**:
|
||||
- `Modules/SqlService.ps1`: Win32 `advapi32.dll`의 `LogonUser` API(Type 9 `LOGON32_LOGON_NEW_CREDENTIALS`, `/netonly`)와 `WindowsIdentity.RunImpersonated`를 결합하여, 사용자가 입력한 AD 도메인 ID/PW를 Windows 보안 토큰으로 위장한 뒤 SQL Server에 `Integrated Security=True`로 인증하는 엔진 구현
|
||||
- `Test-SqlConnection`: SQL Server가 실제로 누구로 인증했는지 `SELECT SUSER_SNAME()`을 질의하여 반환 메시지에 실시간 출력
|
||||
- `Get-SqlDatabases`, `Invoke-SqlBackupAction`, `Invoke-SqlQuerySnippet`: 모든 SQL 작업이 설정된 인증 컨텍스트(`Invoke-WithSqlAuthContext`) 하에서 안전하게 실행되도록 통합
|
||||
- **환경설정 UI (`Views/SettingsView.xaml`) 및 SQL 관리 화면 (`Views/SqlDatabaseView.xaml`) 연동**:
|
||||
- 인증 방식 선택 옵션 3종 확장:
|
||||
1) 현재 PC Windows 통합 인증 (Current Session)
|
||||
2) AD 도메인 계정 위장 Windows 인증 (AD LogonUser /netonly)
|
||||
3) SQL Server 자체 계정 인증 (User ID / Password)
|
||||
- `SettingsView.xaml`: `TxtSqlDomain` (AD 도메인 입력칸), `LblSqlDomain`, 동적 설명 힌트(`TxtSqlAuthHint`) 및 선택 변경에 따른 입력 필드 실시간 활성화/비활성화 처리 연동
|
||||
- `config.json`: `MsSql.Domain` 필드 추가 및 영속적 저장 지원
|
||||
|
||||
## [v1.5.0] - 2026-10-20
|
||||
### Added
|
||||
- **다중 도메인 컨트롤러(6대 DC) 환경 NetApp 활성 DC 감지 및 복제 지연(Replication Lag) 방지 전략 탑재**:
|
||||
|
||||
+209
-82
@@ -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($_) }
|
||||
$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
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
RowCount = $table.Rows.Count
|
||||
Output = "(성공: $($table.Rows.Count)개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n$($sw.ToString())"
|
||||
$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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+93
-7
@@ -228,6 +228,7 @@ $script:loadSettingsToUI = {
|
||||
$txtSqlServer = $v.FindName("TxtSqlServer")
|
||||
$cmbSqlAuth = $v.FindName("CmbSqlAuthType")
|
||||
$txtSqlBackupDir = $v.FindName("TxtSqlBackupDir")
|
||||
$txtSqlDomain = $v.FindName("TxtSqlDomain")
|
||||
$txtSqlUser = $v.FindName("TxtSqlUser")
|
||||
$txtSqlPassword = $v.FindName("TxtSqlPassword")
|
||||
|
||||
@@ -250,7 +251,14 @@ $script:loadSettingsToUI = {
|
||||
|
||||
if ($chkSqlLive) { $chkSqlLive.IsChecked = [bool]$c.MsSql.UseLive }
|
||||
if ($txtSqlServer) { $txtSqlServer.Text = $c.MsSql.Server }
|
||||
if ($cmbSqlAuth) { $cmbSqlAuth.SelectedIndex = if ($c.MsSql.AuthType -eq "Sql") { 1 } else { 0 } }
|
||||
if ($txtSqlDomain) { $txtSqlDomain.Text = if ($c.MsSql.Domain) { $c.MsSql.Domain } else { "corp" } }
|
||||
if ($cmbSqlAuth) {
|
||||
$cmbSqlAuth.SelectedIndex = switch ($c.MsSql.AuthType) {
|
||||
"AdImpersonated" { 1 }
|
||||
"Sql" { 2 }
|
||||
default { 0 }
|
||||
}
|
||||
}
|
||||
if ($txtSqlBackupDir) { $txtSqlBackupDir.Text = $c.MsSql.BackupDirectory }
|
||||
if ($txtSqlUser) { $txtSqlUser.Text = $c.MsSql.Username }
|
||||
if ($txtSqlPassword) { $txtSqlPassword.Password = $c.MsSql.Password }
|
||||
@@ -1152,12 +1160,22 @@ function Navigate-ToPage {
|
||||
$btnTestConn = $view.FindName("BtnTestSqlConnection")
|
||||
$btnBackup = $view.FindName("BtnBackupDb")
|
||||
$btnExecQuery = $view.FindName("BtnExecuteQuery")
|
||||
$cmbSqlAuthInDb = $view.FindName("CmbSqlAuth")
|
||||
|
||||
if ($cmbSqlAuthInDb) {
|
||||
$cmbSqlAuthInDb.SelectedIndex = switch ($global:AppConfig.MsSql.AuthType) {
|
||||
"AdImpersonated" { 1 }
|
||||
"Sql" { 2 }
|
||||
default { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
if ($btnSqlRefresh) { $btnSqlRefresh.add_Click({ & $script:reloadSql }) }
|
||||
|
||||
if ($btnTestConn) {
|
||||
$btnTestConn.add_Click({
|
||||
$res = Test-SqlConnection -Server $global:AppConfig.MsSql.Server -AuthType $global:AppConfig.MsSql.AuthType -Username $global:AppConfig.MsSql.Username -Password $global:AppConfig.MsSql.Password
|
||||
$cfg = $global:AppConfig.MsSql
|
||||
$res = Test-SqlConnection -Server $cfg.Server -AuthType $cfg.AuthType -Domain $cfg.Domain -Username $cfg.Username -Password $cfg.Password
|
||||
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Error }
|
||||
[System.Windows.MessageBox]::Show($res.Message, "MS SQL 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
|
||||
})
|
||||
@@ -1209,6 +1227,58 @@ function Navigate-ToPage {
|
||||
|
||||
& $script:loadSettingsToUI
|
||||
|
||||
$updateSqlAuthUi = {
|
||||
$v = $ViewCache["Settings"]
|
||||
if (-not $v) { return }
|
||||
$cmb = $v.FindName("CmbSqlAuthType")
|
||||
$txtHint = $v.FindName("TxtSqlAuthHint")
|
||||
$lblDomain = $v.FindName("LblSqlDomain")
|
||||
$txtDomain = $v.FindName("TxtSqlDomain")
|
||||
$lblUser = $v.FindName("LblSqlUser")
|
||||
$txtUser = $v.FindName("TxtSqlUser")
|
||||
$lblPass = $v.FindName("LblSqlPassword")
|
||||
$txtPass = $v.FindName("TxtSqlPassword")
|
||||
|
||||
switch ($cmb.SelectedIndex) {
|
||||
1 {
|
||||
# AdImpersonated
|
||||
if ($txtHint) { $txtHint.Text = "입력한 AD 도메인 ID/PW를 Windows 토큰(/netonly)으로 위장하여 SQL Server에 Windows 통합 인증으로 접속합니다." }
|
||||
if ($lblDomain) { $lblDomain.Text = "AD 도메인 (예: corp 또는 corp.local):" }
|
||||
if ($lblUser) { $lblUser.Text = "AD 도메인 계정 ID (예: sql_admin):" }
|
||||
if ($lblPass) { $lblPass.Text = "AD 도메인 계정 암호:" }
|
||||
if ($txtDomain) { $txtDomain.IsEnabled = $true; $txtDomain.Opacity = 1.0 }
|
||||
if ($txtUser) { $txtUser.IsEnabled = $true; $txtUser.Opacity = 1.0 }
|
||||
if ($txtPass) { $txtPass.IsEnabled = $true; $txtPass.Opacity = 1.0 }
|
||||
}
|
||||
2 {
|
||||
# Sql Auth
|
||||
if ($txtHint) { $txtHint.Text = "SQL Server 자체 계정(sa 또는 DB 전용 계정)과 암호로 접속합니다." }
|
||||
if ($lblDomain) { $lblDomain.Text = "AD 도메인 (SQL 인증 시 불필요):" }
|
||||
if ($lblUser) { $lblUser.Text = "SQL Server 계정 ID (예: sa):" }
|
||||
if ($lblPass) { $lblPass.Text = "SQL Server 계정 암호:" }
|
||||
if ($txtDomain) { $txtDomain.IsEnabled = $false; $txtDomain.Opacity = 0.5 }
|
||||
if ($txtUser) { $txtUser.IsEnabled = $true; $txtUser.Opacity = 1.0 }
|
||||
if ($txtPass) { $txtPass.IsEnabled = $true; $txtPass.Opacity = 1.0 }
|
||||
}
|
||||
default {
|
||||
# Integrated (Current Windows User)
|
||||
if ($txtHint) { $txtHint.Text = "현재 윈도우에 로그인된 사용자 자격증명(Kerberos/NTLM)으로 SQL Server에 접속합니다." }
|
||||
if ($lblDomain) { $lblDomain.Text = "AD 도메인 (현재 세션 인증 시 불필요):" }
|
||||
if ($lblUser) { $lblUser.Text = "로그인 계정 (현재 Windows 로그인 토큰 사용):" }
|
||||
if ($lblPass) { $lblPass.Text = "비밀번호 (현재 세션 인증 시 불필요):" }
|
||||
if ($txtDomain) { $txtDomain.IsEnabled = $false; $txtDomain.Opacity = 0.5 }
|
||||
if ($txtUser) { $txtUser.IsEnabled = $false; $txtUser.Opacity = 0.5 }
|
||||
if ($txtPass) { $txtPass.IsEnabled = $false; $txtPass.Opacity = 0.5 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cmbSqlAuth = $view.FindName("CmbSqlAuthType")
|
||||
if ($cmbSqlAuth) {
|
||||
$cmbSqlAuth.add_SelectionChanged({ & $updateSqlAuthUi })
|
||||
}
|
||||
& $updateSqlAuthUi
|
||||
|
||||
# Test AD
|
||||
if ($btnTestAd) {
|
||||
$btnTestAd.add_Click({
|
||||
@@ -1246,11 +1316,16 @@ function Navigate-ToPage {
|
||||
$v = $ViewCache["Settings"]
|
||||
$srv = $v.FindName("TxtSqlServer").Text
|
||||
$cmb = $v.FindName("CmbSqlAuthType")
|
||||
$authType = if ($cmb.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
|
||||
$authType = switch ($cmb.SelectedIndex) {
|
||||
1 { "AdImpersonated" }
|
||||
2 { "Sql" }
|
||||
default { "Integrated" }
|
||||
}
|
||||
$domain = $v.FindName("TxtSqlDomain").Text
|
||||
$user = $v.FindName("TxtSqlUser").Text
|
||||
$pass = $v.FindName("TxtSqlPassword").Password
|
||||
|
||||
$res = Test-SqlConnection -Server $srv -AuthType $authType -Username $user -Password $pass
|
||||
$res = Test-SqlConnection -Server $srv -AuthType $authType -Domain $domain -Username $user -Password $pass
|
||||
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Warning }
|
||||
[System.Windows.MessageBox]::Show($res.Message, "MS SQL 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
|
||||
})
|
||||
@@ -1273,13 +1348,18 @@ function Navigate-ToPage {
|
||||
|
||||
$srv = $v.FindName("TxtSqlServer").Text
|
||||
$cmb = $v.FindName("CmbSqlAuthType")
|
||||
$authType = if ($cmb.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
|
||||
$authType = switch ($cmb.SelectedIndex) {
|
||||
1 { "AdImpersonated" }
|
||||
2 { "Sql" }
|
||||
default { "Integrated" }
|
||||
}
|
||||
$sDomain = $v.FindName("TxtSqlDomain").Text
|
||||
$sUser = $v.FindName("TxtSqlUser").Text
|
||||
$sPass = $v.FindName("TxtSqlPassword").Password
|
||||
|
||||
$adRes = Test-ADConnection -DomainController $dc -BaseOU $ou -Username $user -Password $pass -UseLdaps $ldaps
|
||||
$netappRes = Test-NetAppConnection -ClusterEndpoint $ep -Username $nUser -Password $nPass -IgnoreSsl $ignoreSsl
|
||||
$sqlRes = Test-SqlConnection -Server $srv -AuthType $authType -Username $sUser -Password $sPass
|
||||
$sqlRes = Test-SqlConnection -Server $srv -AuthType $authType -Domain $sDomain -Username $sUser -Password $sPass
|
||||
|
||||
$report = @"
|
||||
=== 전체 인프라 일괄 진단 결과 ===
|
||||
@@ -1305,7 +1385,11 @@ function Navigate-ToPage {
|
||||
$btnSave.add_Click({
|
||||
$v = $ViewCache["Settings"]
|
||||
$cmb = $v.FindName("CmbSqlAuthType")
|
||||
$authType = if ($cmb.SelectedIndex -eq 1) { "Sql" } else { "Integrated" }
|
||||
$authType = switch ($cmb.SelectedIndex) {
|
||||
1 { "AdImpersonated" }
|
||||
2 { "Sql" }
|
||||
default { "Integrated" }
|
||||
}
|
||||
|
||||
$chkGlobalLive = $v.FindName("ChkGlobalLiveMode")
|
||||
$chkAdLive = $v.FindName("ChkAdLive")
|
||||
@@ -1340,6 +1424,7 @@ function Navigate-ToPage {
|
||||
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkSqlLive.IsChecked)
|
||||
Server = $v.FindName("TxtSqlServer").Text
|
||||
AuthType = $authType
|
||||
Domain = $v.FindName("TxtSqlDomain").Text
|
||||
Username = $v.FindName("TxtSqlUser").Text
|
||||
Password = $v.FindName("TxtSqlPassword").Password
|
||||
DefaultDatabase = "master"
|
||||
@@ -1365,6 +1450,7 @@ function Navigate-ToPage {
|
||||
$btnReload.add_Click({
|
||||
$global:AppConfig = Get-AppConfig
|
||||
& $script:loadSettingsToUI
|
||||
& $updateSqlAuthUi
|
||||
Update-ModeBadgeUI
|
||||
[System.Windows.MessageBox]::Show("config.json 설정을 다시 불러왔습니다.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
||||
})
|
||||
|
||||
+10
-5
@@ -123,19 +123,24 @@
|
||||
<TextBox x:Name="TxtSqlServer" Text="localhost" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
|
||||
|
||||
<TextBlock Text="인증 방식:" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
|
||||
<ComboBox x:Name="CmbSqlAuthType" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,12">
|
||||
<ComboBoxItem Content="Windows 통합 인증 (Integrated Security)" />
|
||||
<ComboBoxItem Content="SQL Server 인증 (User ID / Password)" />
|
||||
<ComboBox x:Name="CmbSqlAuthType" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,6">
|
||||
<ComboBoxItem Content="Windows 통합 인증 (현재 Windows 세션 토큰)" />
|
||||
<ComboBoxItem Content="AD 도메인 계정 위장 Windows 인증 (LogonUser /netonly)" />
|
||||
<ComboBoxItem Content="SQL Server 자체 계정 인증 (User ID / Password)" />
|
||||
</ComboBox>
|
||||
<TextBlock x:Name="TxtSqlAuthHint" Text="현재 윈도우에 로그인된 사용자 자격증명으로 SQL Server에 접속합니다." FontSize="11" Foreground="#60A5FA" Margin="0,0,0,12" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Text="백업 파일 기본 저장 디렉토리:" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
|
||||
<TextBox x:Name="TxtSqlBackupDir" Text="C:\Backups" Style="{StaticResource ModernTextBoxStyle}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="12,0,0,0">
|
||||
<TextBlock Text="SQL 계정 (SQL 인증 시):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
|
||||
<TextBlock x:Name="LblSqlDomain" Text="AD 도메인 (AD 위장 인증 시):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
|
||||
<TextBox x:Name="TxtSqlDomain" Text="corp" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
|
||||
|
||||
<TextBlock x:Name="LblSqlUser" Text="로그인 계정 (AD 사용자명 또는 SQL 계정):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
|
||||
<TextBox x:Name="TxtSqlUser" Text="sa" Style="{StaticResource ModernTextBoxStyle}" Margin="0,0,0,12" />
|
||||
|
||||
<TextBlock Text="SQL 암호 (SQL 인증 시):" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
|
||||
<TextBlock x:Name="LblSqlPassword" Text="비밀번호:" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" FontSize="12" />
|
||||
<PasswordBox x:Name="TxtSqlPassword" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource CardBorderBrush}" Height="34" Margin="0,0,0,12" />
|
||||
|
||||
<Button x:Name="BtnTestSql" Content="SQL Server 연결 테스트" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Right" Padding="14,6" />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="220" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="160" />
|
||||
<ColumnDefinition Width="210" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
@@ -36,8 +36,9 @@
|
||||
|
||||
<TextBlock Grid.Column="2" Text="인증 방식:" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="0,0,10,0" />
|
||||
<ComboBox Grid.Column="3" x:Name="CmbSqlAuth" SelectedIndex="0" Height="32" Background="#0F172A" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,16,0">
|
||||
<ComboBoxItem Content="Windows 통합 인증" />
|
||||
<ComboBoxItem Content="SQL Server 인증" />
|
||||
<ComboBoxItem Content="Windows 통합 인증 (현재 세션)" />
|
||||
<ComboBoxItem Content="AD 도메인 계정 위장 Windows 인증" />
|
||||
<ComboBoxItem Content="SQL Server 자체 계정 인증" />
|
||||
</ComboBox>
|
||||
|
||||
<StackPanel Grid.Column="4" Orientation="Horizontal">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"UseLive": false,
|
||||
"Server": "localhost",
|
||||
"AuthType": "Integrated",
|
||||
"Domain": "corp",
|
||||
"Username": "sa",
|
||||
"Password": "",
|
||||
"DefaultDatabase": "master",
|
||||
|
||||
Reference in New Issue
Block a user