diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d74c9..069d5c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) 방지 전략 탑재**: diff --git a/Modules/SqlService.ps1 b/Modules/SqlService.ps1 index 47f5dc6..bfd8acc 100644 --- a/Modules/SqlService.ps1 +++ b/Modules/SqlService.ps1 @@ -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() } } diff --git a/Start-App.ps1 b/Start-App.ps1 index d6e203a..b31f490 100644 --- a/Start-App.ps1 +++ b/Start-App.ps1 @@ -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) }) diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml index ce7200d..6e39615 100644 --- a/Views/SettingsView.xaml +++ b/Views/SettingsView.xaml @@ -123,19 +123,24 @@ - - - + + + + + - + + + + - +