# Modules\ADService.ps1 # Active Directory Service Module (.NET System.DirectoryServices Live & Fallback) function Test-ADConnection { param( [string]$DomainController, [string]$BaseOU, [string]$Username, [string]$Password, [bool]$UseLdaps = $false ) try { $protocol = if ($UseLdaps) { "LDAP://$DomainController:636" } else { "LDAP://$DomainController" } $path = if ([string]::IsNullOrWhiteSpace($BaseOU)) { "$protocol/RootDSE" } else { "$protocol/$BaseOU" } $entry = if ([string]::IsNullOrWhiteSpace($Username)) { New-Object System.DirectoryServices.DirectoryEntry($path) } else { New-Object System.DirectoryServices.DirectoryEntry($path, $Username, $Password) } # 바인딩 강제 테스트 $null = $entry.NativeObject return [PSCustomObject]@{ Success = $true Message = "Active Directory DC ($DomainController) 연결 성공 (RootDSE 응답 확인)" LdapPath = $path } } catch { return [PSCustomObject]@{ Success = $false Message = "AD 연결 실패: $($_.Exception.Message)" LdapPath = $path } } } function Get-ADUserList { param( [string]$Filter = "", [object]$Config = $null ) $useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false } if ($useLive) { $dc = $Config.ActiveDirectory.DomainController $baseOu = $Config.ActiveDirectory.BaseOU $useLdaps = [bool]$Config.ActiveDirectory.UseLdaps $user = $Config.ActiveDirectory.Username $pass = $Config.ActiveDirectory.Password try { $prefix = if ($useLdaps) { "LDAP://$dc:636" } else { "LDAP://$dc" } $rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { $prefix } else { "$prefix/$baseOu" } $rootEntry = if ([string]::IsNullOrWhiteSpace($user)) { New-Object System.DirectoryServices.DirectoryEntry($rootPath) } else { New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass) } $searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry) $searcher.PageSize = 1000 if ([string]::IsNullOrWhiteSpace($Filter)) { $searcher.Filter = "(&(objectCategory=person)(objectClass=user))" } else { $esc = $Filter -replace '[*()\\\x00]', '' $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(|(sAMAccountName=*$esc*)(displayName=*$esc*)(department=*$esc*)))" } $props = @("sAMAccountName", "displayName", "department", "title", "userPrincipalName", "mail", "userAccountControl", "lockoutTime", "lastLogonTimestamp", "distinguishedName") foreach ($p in $props) { [void]$searcher.PropertiesToLoad.Add($p) } $results = $searcher.FindAll() $userList = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($res in $results) { $p = $res.Properties $sam = if ($p["samaccountname"].Count -gt 0) { $p["samaccountname"][0] } else { "" } $disp = if ($p["displayname"].Count -gt 0) { $p["displayname"][0] } else { $sam } $dept = if ($p["department"].Count -gt 0) { $p["department"][0] } else { "-" } $title = if ($p["title"].Count -gt 0) { $p["title"][0] } else { "-" } $upn = if ($p["userprincipalname"].Count -gt 0) { $p["userprincipalname"][0] } elseif ($p["mail"].Count -gt 0) { $p["mail"][0] } else { "$sam@domain.local" } $dn = if ($p["distinguishedname"].Count -gt 0) { $p["distinguishedname"][0] } else { "" } $uac = if ($p["useraccountcontrol"].Count -gt 0) { [int]$p["useraccountcontrol"][0] } else { 512 } $isDisabled = ($uac -band 2) -eq 2 $statusText = if ($isDisabled) { "비활성" } else { "활성" } $lockVal = if ($p["lockouttime"].Count -gt 0) { [int64]$p["lockouttime"][0] } else { 0 } $isLocked = ($lockVal -gt 0) $lockedText = if ($isLocked) { "잠김" } else { "정상" } $lastLogonStr = "-" if ($p["lastlogontimestamp"].Count -gt 0) { try { $fileTime = [int64]$p["lastlogontimestamp"][0] if ($fileTime -gt 0) { $lastLogonStr = [DateTime]::FromFileTime($fileTime).ToString("yyyy-MM-dd HH:mm:ss") } } catch {} } $userList.Add([PSCustomObject]@{ SamAccountName = $sam DisplayName = $disp Department = $dept Title = $title UserPrincipalName = $upn StatusText = $statusText LockedText = $lockedText LastLogonDate = $lastLogonStr Enabled = (-not $isDisabled) IsLocked = $isLocked DistinguishedName = $dn IsLive = $true }) } return $userList.ToArray() } catch { Write-Warning "실제 AD 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)" } } # Fallback / Mock Data return Get-ADSampleUsers -Filter $Filter } function Unlock-ADUserAccount { param( [string]$SamAccountName, [object]$Config = $null ) $useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false } if ($useLive) { $dc = $Config.ActiveDirectory.DomainController $baseOu = $Config.ActiveDirectory.BaseOU $user = $Config.ActiveDirectory.Username $pass = $Config.ActiveDirectory.Password try { $rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc" } else { "LDAP://$dc/$baseOu" } $rootEntry = if ([string]::IsNullOrWhiteSpace($user)) { New-Object System.DirectoryServices.DirectoryEntry($rootPath) } else { New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass) } $searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))" $result = $searcher.FindOne() if ($null -eq $result) { return [PSCustomObject]@{ Success = $false; Message = "AD에서 [$SamAccountName] 사용자를 찾을 수 없습니다." } } $userEntry = $result.GetDirectoryEntry() $userEntry.Properties["lockoutTime"].Value = 0 $userEntry.CommitChanges() return [PSCustomObject]@{ Success = $true Message = "[$SamAccountName] 계정의 잠금이 실제 AD에서 해제되었습니다." } } catch { return [PSCustomObject]@{ Success = $false Message = "AD 잠금 해제 실패: $($_.Exception.Message)" } } } # Demo Simulation return [PSCustomObject]@{ Success = $true Message = "[데모 모드] [$SamAccountName] 계정의 잠금이 해제되었습니다." } } function Reset-ADUserPassword { param( [string]$SamAccountName, [string]$NewPassword = "Passw0rd!#$((Get-Random -Minimum 1000 -Maximum 9999))", [object]$Config = $null ) $useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false } if ($useLive) { $dc = $Config.ActiveDirectory.DomainController $baseOu = $Config.ActiveDirectory.BaseOU $user = $Config.ActiveDirectory.Username $pass = $Config.ActiveDirectory.Password try { $rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc:636" } else { "LDAP://$dc:636/$baseOu" } $rootEntry = if ([string]::IsNullOrWhiteSpace($user)) { New-Object System.DirectoryServices.DirectoryEntry($rootPath) } else { New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass) } $searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))" $result = $searcher.FindOne() if ($null -eq $result) { return [PSCustomObject]@{ Success = $false; Message = "AD에서 [$SamAccountName] 사용자를 찾을 수 없습니다." } } $userEntry = $result.GetDirectoryEntry() $userEntry.Invoke("SetPassword", @($NewPassword)) $userEntry.Properties["pwdLastSet"].Value = 0 # 다음 로그온 시 암호 변경 강제 $userEntry.CommitChanges() return [PSCustomObject]@{ Success = $true Message = "[$SamAccountName] 암호가 성공적으로 초기화되었습니다.`n임시 암호: $NewPassword" } } catch { return [PSCustomObject]@{ Success = $false Message = "AD 암호 초기화 실패: $($_.Exception.Message)`n(참고: AD 암호 변경은 보안상 LDAPS 636 포트 연결이 필요합니다.)" } } } return [PSCustomObject]@{ Success = $true Message = "[데모 모드] [$SamAccountName] 계정의 암호가 초기화되었습니다.`n임시 암호: $NewPassword" } } function Set-ADUserAccountState { param( [string]$SamAccountName, [bool]$Enable, [object]$Config = $null ) $useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false } if ($useLive) { $dc = $Config.ActiveDirectory.DomainController $baseOu = $Config.ActiveDirectory.BaseOU $user = $Config.ActiveDirectory.Username $pass = $Config.ActiveDirectory.Password try { $rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc" } else { "LDAP://$dc/$baseOu" } $rootEntry = if ([string]::IsNullOrWhiteSpace($user)) { New-Object System.DirectoryServices.DirectoryEntry($rootPath) } else { New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass) } $searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))" $result = $searcher.FindOne() if ($null -eq $result) { return [PSCustomObject]@{ Success = $false; Message = "사용자를 찾을 수 없습니다." } } $userEntry = $result.GetDirectoryEntry() $uac = [int]$userEntry.Properties["userAccountControl"].Value if ($Enable) { $userEntry.Properties["userAccountControl"].Value = $uac -band (-bnot 2) } else { $userEntry.Properties["userAccountControl"].Value = $uac -bor 2 } $userEntry.CommitChanges() $action = if ($Enable) { "활성화" } else { "비활성화" } return [PSCustomObject]@{ Success = $true; Message = "[$SamAccountName] 계정이 $action 되었습니다." } } catch { return [PSCustomObject]@{ Success = $false; Message = "AD 상태 변경 실패: $($_.Exception.Message)" } } } $action = if ($Enable) { "활성화" } else { "비활성화" } return [PSCustomObject]@{ Success = $true; Message = "[데모 모드] [$SamAccountName] 계정이 $action 되었습니다." } } function Get-ADSampleUsers { param([string]$Filter = "") $users = @( [PSCustomObject]@{ SamAccountName = "gdhong" DisplayName = "홍길동" Department = "클라우드인프라팀" Title = "수석 엔지니어" UserPrincipalName = "gdhong@corp.local" StatusText = "활성" LockedText = "정상" LastLogonDate = "2026-10-20 09:14:22" Enabled = $true IsLocked = $false DistinguishedName = "CN=홍길동,OU=Engineers,OU=Users,DC=corp,DC=local" IsLive = $false }, [PSCustomObject]@{ SamAccountName = "cskim" DisplayName = "김철수" Department = "데이터플랫폼팀" Title = "DBA" UserPrincipalName = "cskim@corp.local" StatusText = "활성" LockedText = "정상" LastLogonDate = "2026-10-20 11:45:01" Enabled = $true IsLocked = $false DistinguishedName = "CN=김철수,OU=DBA,OU=Users,DC=corp,DC=local" IsLive = $false }, [PSCustomObject]@{ SamAccountName = "yylee" DisplayName = "이영희" Department = "보안관제팀" Title = "정보보안책임" UserPrincipalName = "yylee@corp.local" StatusText = "잠김" LockedText = "잠김 (5회 실패)" LastLogonDate = "2026-10-19 18:22:10" Enabled = $true IsLocked = $true DistinguishedName = "CN=이영희,OU=Security,OU=Users,DC=corp,DC=local" IsLive = $false }, [PSCustomObject]@{ SamAccountName = "mschoi" DisplayName = "최민수" Department = "개발운영팀" Title = "DevOps 엔지니어" UserPrincipalName = "mschoi@corp.local" StatusText = "비활성" LockedText = "정상" LastLogonDate = "2026-09-01 14:02:00" Enabled = $false IsLocked = $false DistinguishedName = "CN=최민수,OU=DevOps,OU=Users,DC=corp,DC=local" IsLive = $false }, [PSCustomObject]@{ SamAccountName = "jhbak" DisplayName = "박지훈" Department = "시스템관리팀" Title = "스토리지 관리자" UserPrincipalName = "jhbak@corp.local" StatusText = "활성" LockedText = "정상" LastLogonDate = "2026-10-20 13:02:15" Enabled = $true IsLocked = $false DistinguishedName = "CN=박지훈,OU=Admins,OU=Users,DC=corp,DC=local" IsLive = $false } ) if ([string]::IsNullOrWhiteSpace($Filter)) { return $users } else { return @($users | Where-Object { $_.SamAccountName -like "*$Filter*" -or $_.DisplayName -like "*$Filter*" -or $_.Department -like "*$Filter*" }) } }