# 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*" }) } } function Get-ADDomainControllers { param( [object]$Config = $null ) $useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false } if ($useLive) { try { if (Get-Command Get-ADDomainController -ErrorAction SilentlyContinue) { $dcs = Get-ADDomainController -Filter * $list = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($d in $dcs) { $list.Add([PSCustomObject]@{ HostName = $d.HostName IPv4Address = $d.IPv4Address Site = $d.Site OperatingSystem = $d.OperatingSystem IsPdc = ($d.OperationMasterRoles -contains "PDCEmulator") IsLive = $true }) } return $list.ToArray() } } catch { Write-Warning "실제 AD DC 목록 조회 실패: $($_.Exception.Message)" } } # 6개 다중 DC 환경 모사 데이터 return @( [PSCustomObject]@{ HostName = "dc01.corp.local"; IPv4Address = "10.10.1.11"; Site = "Seoul-HQ"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $true; IsLive = $false }, [PSCustomObject]@{ HostName = "dc02.corp.local"; IPv4Address = "10.10.1.12"; Site = "Seoul-HQ"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $false; IsLive = $false }, [PSCustomObject]@{ HostName = "dc03.corp.local"; IPv4Address = "10.10.1.13"; Site = "Seoul-HQ"; OperatingSystem = "Windows Server 2019 Datacenter"; IsPdc = $false; IsLive = $false }, [PSCustomObject]@{ HostName = "dc04.corp.local"; IPv4Address = "10.20.1.14"; Site = "Pangyo-DC"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $false; IsLive = $false }, [PSCustomObject]@{ HostName = "dc05.corp.local"; IPv4Address = "10.30.1.15"; Site = "Daejeon-DR"; OperatingSystem = "Windows Server 2019 Datacenter"; IsPdc = $false; IsLive = $false }, [PSCustomObject]@{ HostName = "dc06.corp.local"; IPv4Address = "10.40.1.16"; Site = "Azure-KOR"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $false; IsLive = $false } ) } function New-ADSecurityGroupOnTargetDc { param( [string]$GroupName, [string]$GroupScope = "Global", [string]$Description = "ModernAdminStudio 자동 생성 보안그룹", [string]$TargetDc = "", [string]$TargetOU = "", [string[]]$Members = @(), [object]$Config = $null ) $useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false } if (-not $TargetDc) { $TargetDc = if ($Config -and $Config.ActiveDirectory) { $Config.ActiveDirectory.DomainController } else { "dc04.corp.local" } } if (-not $TargetOU) { $TargetOU = if ($Config -and $Config.ActiveDirectory) { $Config.ActiveDirectory.BaseOU } else { "OU=SecurityGroups,DC=corp,DC=local" } } if ($useLive) { try { $user = $Config.ActiveDirectory.Username $pass = $Config.ActiveDirectory.Password $useLdaps = [bool]$Config.ActiveDirectory.UseLdaps $proto = if ($useLdaps) { "LDAP://$TargetDc:636" } else { "LDAP://$TargetDc" } $ouPath = "$proto/$TargetOU" $ouEntry = if ([string]::IsNullOrWhiteSpace($user)) { New-Object System.DirectoryServices.DirectoryEntry($ouPath) } else { New-Object System.DirectoryServices.DirectoryEntry($ouPath, $user, $pass) } # 그룹 생성 $groupEntry = $ouEntry.Children.Add("CN=$GroupName", "group") $groupEntry.Properties["sAMAccountName"].Value = $GroupName $groupEntry.Properties["description"].Value = $Description # Global Security Group Type: -2147483646 (0x80000002) $groupType = switch ($GroupScope) { "DomainLocal" { -2147483644 } # 0x80000004 "Universal" { -2147483640 } # 0x80000008 default { -2147483646 } # Global } $groupEntry.Properties["groupType"].Value = $groupType $groupEntry.CommitChanges() # 멤버 추가 $addedMembers = [System.Collections.Generic.List[string]]::new() if ($Members -and $Members.Count -gt 0) { foreach ($m in $Members) { if ([string]::IsNullOrWhiteSpace($m)) { continue } try { # TargetDc 상에서 사용자 검색 $searcher = New-Object System.DirectoryServices.DirectorySearcher($ouEntry) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$m))" $found = $searcher.FindOne() if ($found) { $userDn = $found.Properties["distinguishedname"][0] $groupEntry.Properties["member"].Add($userDn) $addedMembers.Add($m) } } catch { Write-Warning "사용자 [$m] 그룹 멤버 추가 중 오류: $($_.Exception.Message)" } } $groupEntry.CommitChanges() } return [PSCustomObject]@{ Success = $true Message = "타겟 DC [$TargetDc]에 보안그룹 [$GroupName] 직접 생성 완료 (멤버 $($addedMembers.Count)명 추가)" TargetDc = $TargetDc GroupDN = "CN=$GroupName,$TargetOU" AddedMembers = $addedMembers.ToArray() IsLive = $true } } catch { return [PSCustomObject]@{ Success = $false Message = "타겟 DC [$TargetDc]에 그룹 생성 실패: $($_.Exception.Message)" TargetDc = $TargetDc IsLive = $true } } } # Mock / Fallback return [PSCustomObject]@{ Success = $true Message = "[Direct DC Binding] 타겟 DC [$TargetDc]에 보안그룹 [$GroupName] 직접 생성 성공 (NetApp 복제 대기 0초 달성)" TargetDc = $TargetDc GroupDN = "CN=$GroupName,$TargetOU" AddedMembers = $Members IsLive = $false } } function Sync-ADObjectToTargetDc { param( [string]$ObjectDN, [string]$SourceDc = "dc01.corp.local", [string]$DestinationDc = "dc04.corp.local", [object]$Config = $null ) $useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false } if ($useLive) { try { # 1. PowerShell ActiveDirectory Module의 Sync-ADObject 시도 if (Get-Command Sync-ADObject -ErrorAction SilentlyContinue) { Sync-ADObject -Object $ObjectDN -Destination $DestinationDc -Source $SourceDc -ErrorAction Stop return [PSCustomObject]@{ Success = $true Message = "Sync-ADObject 완료: [$ObjectDN] ($SourceDc -> $DestinationDc 즉시 단방향 복제)" SourceDc = $SourceDc DestinationDc = $DestinationDc Method = "Sync-ADObject" IsLive = $true } } # 2. repadmin 명령행 도구 폴백 시도 $repOutput = & repadmin.exe /replicate $DestinationDc $SourceDc $ObjectDN 2>&1 if ($LASTEXITCODE -eq 0) { return [PSCustomObject]@{ Success = $true Message = "repadmin 복제 성공 ($SourceDc -> $DestinationDc)" SourceDc = $SourceDc DestinationDc = $DestinationDc Method = "repadmin /replicate" IsLive = $true } } else { return [PSCustomObject]@{ Success = $false Message = "repadmin 실패: $repOutput" SourceDc = $SourceDc DestinationDc = $DestinationDc Method = "repadmin /replicate" IsLive = $true } } } catch { return [PSCustomObject]@{ Success = $false Message = "AD 개체 대상 복제 실패: $($_.Exception.Message)" SourceDc = $SourceDc DestinationDc = $DestinationDc IsLive = $true } } } # Mock / Fallback return [PSCustomObject]@{ Success = $true Message = "[Targeted Sync] 개체 [$ObjectDN]가 소스 DC [$SourceDc]에서 목적지 DC [$DestinationDc]로 즉시 단방향 복제되었습니다." SourceDc = $SourceDc DestinationDc = $DestinationDc Method = "Targeted Replication (Simulated)" IsLive = $false } }