379 lines
15 KiB
PowerShell
379 lines
15 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)
|
|
}
|
|
|
|
# 바인딩 강제 테스트
|
|
$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*"
|
|
})
|
|
}
|
|
}
|