592 lines
25 KiB
PowerShell
592 lines
25 KiB
PowerShell
# 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)
|
|
}
|
|
|
|
# 바인딩 강제 테스트
|
|
if ($null -eq $entry.NativeObject) {
|
|
throw "Active Directory DC에 바인딩할 수 없습니다. (NativeObject null)"
|
|
}
|
|
$null = $entry.Guid
|
|
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 {
|
|
Write-Verbose "lastLogonTimestamp 파싱 건너뜀: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
$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 {
|
|
$matched = @($users | Where-Object {
|
|
$_.SamAccountName -like "*$Filter*" -or
|
|
$_.DisplayName -like "*$Filter*" -or
|
|
$_.Department -like "*$Filter*"
|
|
})
|
|
return ,$matched
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|