Özel Çözüm: Active Directory İstemci Oturumları (Bölüm 1) – Politika Tabanlı Erişim Otomasyonu

İçerik Türü
Üreticiler
Sürümler
WIN-11-ENTWS-2025-STD

AI Ön İzleme

Bu içerik sana uygun mu? Kısa ön izleme ile hızlıca karar ver.

Techaptic’e hoş geldiniz!

Bu özel çözümde, Active Directory'ye dahil istemci bilgisayarlarda hangi kullanıcıların veya güvenlik gruplarının yerel olarak oturum açabileceğini merkezi biçimde yönetebileceğimiz bir yapı oluşturuyoruz. Kullanıcı veya bilgisayar başına ayrı GPO'lar oluşturmak yerine politika durumu ve izin verilen hesaplar özel Active Directory attribute'ları üzerinden bilgisayar objelerinde tutuluyor ve yönetim PowerShell tabanlı bir yönetici konsoluyla gerçekleştiriliyor.

İstemci tarafında ise GPO ile dağıtılan Startup Script, zamanlanmış bir görev oluşturarak güncel ayarları Active Directory'den düzenli olarak okuyor, yerel policy cache ve geri dönüş baseline'ını koruyor ve gerekli Allow log on locally yapılandırmasını uyguluyor. Ayrıca zamanlanmış senkronizasyonu doğruluyor, yetkisiz ve destek grubu oturum açma senaryolarını test ediyor, önceki kullanıcının erişimini kaldırıyor ve istisnai durumlarda kullanılabilecek rebaseline modunu gösteriyoruz.

🔗 Yardımcı Linkler
  • Bu bölümde paylaşılan bir link bulunmamaktadır.

Video Anlatımı

Kullanılan Komutlar


ADUC Şema Önbelleğini Yenileme

Get-Process mmc -ErrorAction SilentlyContinue | Stop-Process -Force

$UserCache = "$env:LOCALAPPDATAMicrosoftWindowsSchCache"

if (Test-Path $UserCache) {
    Rename-Item `
        -Path $UserCache `
        -NewName ("SchCache.backup-{0}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
}

Workstation Access Engine Dağıtımı

<#
.SYNOPSIS
Techaptic - Workstation Access Engine

.DESCRIPTION
This Techaptic script manages local interactive logon access on domain-joined Windows
workstations by reading two custom Active Directory computer attributes:

- techapticLogonPolicyEnabled
- techapticLogonPrincipals

The engine is designed to be deployed as a Group Policy PowerShell startup script.
Install mode copies the engine to the protected local Techaptic directory, captures the
pre-Techaptic Allow log on locally baseline when needed, creates the scheduled sync task,
and performs an initial synchronization.

Sync mode reads the current computer policy from Active Directory, resolves configured
users and security groups to SIDs, refreshes the local policy cache, and applies the
required SeInteractiveLogonRight state. If Active Directory cannot be reached, the engine
uses the last successfully cached policy.

Rebaseline mode explicitly replaces the stored baseline with the computer's current
Allow log on locally state. Rebaseline is blocked while the Techaptic workstation policy
is enabled.

.PARAMETER Mode
Install
    Installs or repairs the local engine, protects the working directory, captures the
    initial baseline if required, registers the scheduled sync task, and performs a sync.

Sync
    Reads the current Active Directory policy and reconciles the local
    SeInteractiveLogonRight state. This is the normal scheduled-task mode.

Rebaseline
    Re-captures the current local SeInteractiveLogonRight state as the new baseline.
    The Techaptic workstation policy must not be enabled.

.EXAMPLE
.Techaptic-Workstation-Access-Engine.ps1 -Mode Install

.EXAMPLE
.Techaptic-Workstation-Access-Engine.ps1 -Mode Sync

.EXAMPLE
.Techaptic-Workstation-Access-Engine.ps1 -Mode Rebaseline

.NOTES
Author      : Techaptic
Maintainer  : Techaptic
Script      : Workstation Access Engine
Execution   : Group Policy PowerShell Startup Script / LocalSystem scheduled task
Local Path  : C:ProgramDataTechapticWorkstationAccessTechaptic-Workstation-Access-Engine.ps1
State Files : baseline.json, policy-cache.json, engine.log

IMPORTANT OPERATIONAL NOTES:
- Deploy this script only to the intended workstation computer OU.
- The engine manages the Windows "Allow log on locally" user right
  (SeInteractiveLogonRight). Avoid competing GPO, MDM, or script configuration for the
  same right.
- "Deny log on locally" takes precedence over "Allow log on locally" and should be reviewed
  for conflicts.
- BUILTINAdministrators is always retained while the Techaptic policy is enabled.
- A successful transition from Enabled to Disabled/Not Set restores the captured baseline
  once. While the policy remains disabled, Techaptic no longer continuously enforces that
  baseline.
- The baseline preserves the exported SeInteractiveLogonRight entries so SID-based and
  account-name entries can be restored without changing their security-template format.
- If Active Directory policy reading fails, the last successful local policy cache is used.
- If a current AD principal cannot be resolved, only a cached SID for that same current DN
  may be reused. Principals removed from the current AD attribute are never resurrected from
  an older cache.
#>

param(
    [ValidateSet('Install','Sync','Rebaseline')]
    [string]$Mode = 'Sync'
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2.0

$WorkDir            = 'C:ProgramDataTechapticWorkstationAccess'
$LocalScript        = Join-Path $WorkDir 'Techaptic-Workstation-Access-Engine.ps1'
$LogFile            = Join-Path $WorkDir 'engine.log'
$CacheFile          = Join-Path $WorkDir 'policy-cache.json'
$BaselineFile       = Join-Path $WorkDir 'baseline.json'
$SyncTaskName       = 'Techaptic Workstation Access - Policy Sync'
$LegacyTempPrefix   = 'Techaptic Workstation Access - Temporary - '
$SyncMinutes        = 30
$MaxLogBytes        = 5MB
$LogArchiveCount    = 3
$MutexName          = 'GlobalTechapticWorkstationAccessEngine'
$AdministratorsSid  = 'S-1-5-32-544'
$LocalSystemSid     = 'S-1-5-18'

$script:Mutex = $null
$script:MutexHeld = $false

function Assert-Elevated {
    $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
    $isSystem = ($identity.User -and $identity.User.Value -eq $LocalSystemSid)
    $isAdmin = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not ($isSystem -or $isAdmin)) {
        throw 'The engine must run as LocalSystem or from an elevated administrator session.'
    }
}

function Initialize-WorkDirectory {
    New-Item -Path $WorkDir -ItemType Directory -Force | Out-Null
}

function Protect-WorkDirectory {
    $acl = New-Object System.Security.AccessControl.DirectorySecurity
    $acl.SetAccessRuleProtection($true, $false)

    $systemSid = New-Object System.Security.Principal.SecurityIdentifier($LocalSystemSid)
    $adminsSid = New-Object System.Security.Principal.SecurityIdentifier($AdministratorsSid)
    $rights = [System.Security.AccessControl.FileSystemRights]::FullControl
    $inherit = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
    $propagation = [System.Security.AccessControl.PropagationFlags]::None
    $allow = [System.Security.AccessControl.AccessControlType]::Allow

    [void]$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($systemSid,$rights,$inherit,$propagation,$allow)))
    [void]$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($adminsSid,$rights,$inherit,$propagation,$allow)))
    $acl.SetOwner($adminsSid)
    Set-Acl -Path $WorkDir -AclObject $acl
}

function Enter-EngineMutex {
    $script:Mutex = New-Object System.Threading.Mutex($false, $MutexName)
    try {
        $script:MutexHeld = $script:Mutex.WaitOne([TimeSpan]::FromMinutes(2))
    }
    catch [System.Threading.AbandonedMutexException] {
        $script:MutexHeld = $true
    }
    if (-not $script:MutexHeld) {
        throw 'Another engine instance is still running.'
    }
}

function Exit-EngineMutex {
    if ($script:Mutex) {
        if ($script:MutexHeld) {
            try { $script:Mutex.ReleaseMutex() } catch { }
        }
        try { $script:Mutex.Dispose() } catch { }
    }
}

function Rotate-LogIfNeeded {
    if (-not (Test-Path $LogFile)) { return }
    try {
        if ((Get-Item $LogFile).Length -lt $MaxLogBytes) { return }
        for ($i = $LogArchiveCount; $i -ge 1; $i--) {
            $current = "$LogFile.$i"
            if ($i -eq $LogArchiveCount) {
                Remove-Item $current -Force -ErrorAction SilentlyContinue
            }
            elseif (Test-Path $current) {
                Move-Item $current "$LogFile.$($i + 1)" -Force
            }
        }
        Move-Item $LogFile "$LogFile.1" -Force
    }
    catch { }
}

function Write-Log {
    param([Parameter(Mandatory)][string]$Message)

    Rotate-LogIfNeeded
    $line = '{0} [{1}] [{2}] {3}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz'), $env:COMPUTERNAME, $Mode, $Message
    Add-Content -Path $LogFile -Value $line -Encoding UTF8

    $color = [ConsoleColor]::Gray
    if ($Message -like 'ERROR:*') {
        $color = [ConsoleColor]::Red
    }
    elseif ($Message -like 'WARNING:*') {
        $color = [ConsoleColor]::Yellow
    }
    elseif (
        $Message -like 'Applied *' -or
        $Message -like 'Restored *' -or
        $Message -like 'Captured *' -or
        $Message -like 'Installed *' -or
        $Message -like 'Registered *' -or
        $Message -like '*completed successfully*'
    ) {
        $color = [ConsoleColor]::Green
    }

    Write-Host $line -ForegroundColor $color
}

function Write-JsonAtomic {
    param(
        [Parameter(Mandatory)]$InputObject,
        [Parameter(Mandatory)][string]$Path,
        [int]$Depth = 6
    )
    $temp = Join-Path (Split-Path $Path -Parent) ('.{0}.{1}.tmp' -f (Split-Path $Path -Leaf), [guid]::NewGuid().ToString('N'))
    try {
        $InputObject | ConvertTo-Json -Depth $Depth | Set-Content -Path $temp -Encoding UTF8
        if (Test-Path $Path) {
            try { [System.IO.File]::Replace($temp, $Path, $null) }
            catch {
                Remove-Item $Path -Force
                Move-Item $temp $Path -Force
            }
        }
        else {
            Move-Item $temp $Path -Force
        }
    }
    finally {
        Remove-Item $temp -Force -ErrorAction SilentlyContinue
    }
}

function Close-Disposable {
    param($Object)
    if ($null -eq $Object) { return }
    try {
        if ($Object -is [System.DirectoryServices.DirectoryEntry]) {
            try { $Object.Close() } catch { }
        }
        ([System.IDisposable]$Object).Dispose()
    }
    catch { }
}

function Escape-LdapFilterValue {
    param([Parameter(Mandatory)][string]$Value)
    $Value.Replace('','5c').Replace('*','2a').Replace('(','28').Replace(')','29').Replace(([char]0).ToString(),'0')
}

function Convert-ToBoolean {
    param($Value)
    if ($Value -is [bool]) { return $Value }
    return ([string]$Value).Trim().Equals('TRUE',[System.StringComparison]::OrdinalIgnoreCase)
}

function Get-PrincipalFromDn {
    param([Parameter(Mandatory)][string]$DistinguishedName)
    $entry = $null
    try {
        $entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DistinguishedName")
        $entry.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::Secure
        $entry.RefreshCache(@('objectSid','sAMAccountName'))
        $sidBytes = [byte[]]$entry.Properties['objectSid'].Value
        if (-not $sidBytes) { throw "objectSid is missing for $DistinguishedName" }
        $sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes,0)
        [pscustomobject]@{
            DistinguishedName = $DistinguishedName
            SamAccountName    = [string]$entry.Properties['sAMAccountName'].Value
            Sid               = $sid.Value
            ObjectClass       = [string]$entry.SchemaClassName
        }
    }
    finally {
        Close-Disposable $entry
    }
}

function Get-TechapticComputerPolicy {
    $root = $null
    $searchRoot = $null
    $searcher = $null
    try {
        $root = New-Object System.DirectoryServices.DirectoryEntry('LDAP://RootDSE')
        $root.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::Secure
        $defaultNamingContext = [string]$root.Properties['defaultNamingContext'].Value
        if ([string]::IsNullOrWhiteSpace($defaultNamingContext)) {
            throw 'Unable to determine defaultNamingContext from RootDSE.'
        }

        $searchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$defaultNamingContext")
        $searchRoot.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::Secure
        $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot)
        $sam = Escape-LdapFilterValue("$($env:COMPUTERNAME)`$")
        $searcher.Filter = "(&(objectCategory=computer)(sAMAccountName=$sam))"
        $searcher.PageSize = 1
        @('distinguishedName','techapticLogonPolicyEnabled','techapticLogonPrincipals') |
            ForEach-Object { [void]$searcher.PropertiesToLoad.Add($_) }

        $result = $searcher.FindOne()
        if (-not $result) {
            throw "Computer object for $env:COMPUTERNAME was not found in Active Directory."
        }

        $p = $result.Properties
        $enabled = $false
        if ($p['techapticlogonpolicyenabled'].Count -gt 0) {
            $enabled = Convert-ToBoolean $p['techapticlogonpolicyenabled'][0]
        }

        $principalDns = @()
        if ($p['techapticlogonprincipals'].Count -gt 0) {
            $principalDns = @($p['techapticlogonprincipals'] | ForEach-Object { [string]$_ })
        }

        [pscustomobject]@{
            ComputerDn   = [string]$p['distinguishedname'][0]
            Enabled      = $enabled
            PrincipalDns = $principalDns
        }
    }
    finally {
        Close-Disposable $searcher
        Close-Disposable $searchRoot
        Close-Disposable $root
    }
}

function Get-SeInteractiveLogonEntries {
    $tmp = Join-Path $WorkDir ('rights-{0}.inf' -f [guid]::NewGuid().ToString('N'))
    try {
        & secedit.exe /export /cfg $tmp /areas USER_RIGHTS /quiet | Out-Null
        if ($LASTEXITCODE -ne 0) { throw "secedit export failed with exit code $LASTEXITCODE" }

        $line = Get-Content $tmp | Where-Object { $_ -match '^SeInteractiveLogonRights*=' } | Select-Object -First 1
        if (-not $line) { return @() }

        $value = ($line -split '=',2)[1].Trim()
        if ([string]::IsNullOrWhiteSpace($value)) { return @() }

        return @(
            $value -split ',' |
                ForEach-Object { $_.Trim() } |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )
    }
    finally {
        Remove-Item $tmp -Force -ErrorAction SilentlyContinue
    }
}

function Convert-SeInteractiveLogonEntryToComparableValue {
    param([Parameter(Mandatory)][string]$Entry)

    $value = $Entry.Trim()
    if ([string]::IsNullOrWhiteSpace($value)) { return $null }

    if ($value.StartsWith('*')) {
        $sidCandidate = $value.Substring(1)
        if ($sidCandidate -match '^S-d-(?:d+-)*d+$') {
            return $sidCandidate.ToUpperInvariant()
        }

        # A leading asterisk denotes a SID-form security-template entry. If the
        # remaining value is not a SID, keep it distinct rather than treating it
        # as an account name during comparison.
        return ('RAW:{0}' -f $value.ToUpperInvariant())
    }

    if ($value -match '^S-d-(?:d+-)*d+$') {
        return $value.ToUpperInvariant()
    }

    try {
        $account = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $value
        $sid = $account.Translate([System.Security.Principal.SecurityIdentifier])
        return $sid.Value.ToUpperInvariant()
    }
    catch {
        return ('NAME:{0}' -f $value.ToUpperInvariant())
    }
}

function Get-SeInteractiveLogonComparableValues {
    param([Parameter(Mandatory)][string[]]$Entries)

    $values = @()
    foreach ($entry in @($Entries)) {
        if ([string]::IsNullOrWhiteSpace([string]$entry)) { continue }
        $comparable = Convert-SeInteractiveLogonEntryToComparableValue -Entry ([string]$entry)
        if (-not [string]::IsNullOrWhiteSpace([string]$comparable)) {
            $values += $comparable
        }
    }

    return @($values | Sort-Object -Unique)
}

function Write-Baseline {
    param(
        [Parameter(Mandatory)][string[]]$Entries,
        [switch]$ReplaceExisting
    )

    $cleanEntries = @(
        $Entries |
            ForEach-Object { ([string]$_).Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
            Select-Object -Unique
    )

    if ($cleanEntries.Count -eq 0) {
        throw 'Refusing to save an empty SeInteractiveLogonRight baseline.'
    }

    if ((Test-Path $BaselineFile) -and $ReplaceExisting) {
        $backup = '{0}.bak-{1}' -f $BaselineFile,(Get-Date -Format 'yyyyMMdd-HHmmss')
        Copy-Item -Path $BaselineFile -Destination $backup -Force
        Write-Log "Backed up previous baseline to $backup"
    }

    $baseline = [ordered]@{
        SchemaVersion = 2
        ComputerName  = $env:COMPUTERNAME
        CapturedUtc   = [datetime]::UtcNow.ToString('o')
        Entries       = $cleanEntries
    }

    Write-JsonAtomic -InputObject $baseline -Path $BaselineFile -Depth 4
    Write-Log "Captured baseline SeInteractiveLogonRight in $BaselineFile"
}

function Save-BaselineIfNeeded {
    if (Test-Path $BaselineFile) {
        try {
            $existing = Get-Content $BaselineFile -Raw | ConvertFrom-Json
            $computerProperty = $existing.PSObject.Properties['ComputerName']
            if ($computerProperty -and $computerProperty.Value -and ([string]$computerProperty.Value -ne $env:COMPUTERNAME)) {
                throw "Baseline belongs to '$($computerProperty.Value)', not '$env:COMPUTERNAME'."
            }
        }
        catch {
            throw "Existing baseline file is invalid or unsafe to use: $($_.Exception.Message)"
        }
        return
    }

    $entries = @(Get-SeInteractiveLogonEntries)
    Write-Baseline -Entries $entries
}

function Apply-SeInteractiveLogonEntries {
    param([Parameter(Mandatory)][string[]]$Entries)

    $desiredEntries = @(
        $Entries |
            ForEach-Object { ([string]$_).Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
            Select-Object -Unique
    )

    if ($desiredEntries.Count -eq 0) {
        throw 'Refusing to apply an empty SeInteractiveLogonRight.'
    }

    $currentEntries = @(Get-SeInteractiveLogonEntries)
    $desiredComparable = @(Get-SeInteractiveLogonComparableValues -Entries $desiredEntries)
    $currentComparable = @(Get-SeInteractiveLogonComparableValues -Entries $currentEntries)

    $diff = @(Compare-Object -ReferenceObject $currentComparable -DifferenceObject $desiredComparable)
    if ($diff.Count -eq 0) {
        Write-Log ("SeInteractiveLogonRight already matches desired state ({0} entry/entries)." -f $desiredEntries.Count)
        return
    }

    $id = [guid]::NewGuid().ToString('N')
    $cfg = Join-Path $WorkDir "apply-rights-$id.inf"
    $db  = Join-Path $WorkDir "apply-rights-$id.sdb"
    $entryText = $desiredEntries -join ','
    $inf = @"
[Unicode]
Unicode=yes
[Version]
signature="`$CHICAGO`$"
Revision=1
[Privilege Rights]
SeInteractiveLogonRight = $entryText
"@

    try {
        Set-Content -Path $cfg -Value $inf -Encoding Unicode
        & secedit.exe /configure /db $db /cfg $cfg /areas USER_RIGHTS /quiet | Out-Null
        if ($LASTEXITCODE -ne 0) { throw "secedit configure failed with exit code $LASTEXITCODE" }

        $actualEntries = @(Get-SeInteractiveLogonEntries)
        $actualComparable = @(Get-SeInteractiveLogonComparableValues -Entries $actualEntries)
        $verifyDiff = @(Compare-Object -ReferenceObject $actualComparable -DifferenceObject $desiredComparable)
        if ($verifyDiff.Count -ne 0) {
            throw 'SeInteractiveLogonRight verification failed after secedit configuration.'
        }

        Write-Log ("Applied and verified SeInteractiveLogonRight with {0} entry/entries." -f $desiredEntries.Count)
    }
    finally {
        Get-ChildItem -LiteralPath $WorkDir -Force -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -like "apply-rights-$id*" } |
            Remove-Item -Force -ErrorAction SilentlyContinue
    }
}

function Apply-SeInteractiveLogonRight {
    param([Parameter(Mandatory)][string[]]$Sids)

    $desiredSids = @($Sids | Where-Object { $_ } | Sort-Object -Unique)
    if ($desiredSids.Count -eq 0) { throw 'Refusing to apply an empty SeInteractiveLogonRight.' }

    foreach ($sid in $desiredSids) {
        if ([string]$sid -notmatch '^S-d-(?:d+-)*d+$') {
            throw "Invalid SID supplied for Techaptic-managed SeInteractiveLogonRight: $sid"
        }
    }

    $entries = @($desiredSids | ForEach-Object { "*$_" })
    Apply-SeInteractiveLogonEntries -Entries $entries
}

function Restore-Baseline {
    if (-not (Test-Path $BaselineFile)) {
        Write-Log 'Policy disabled, but no baseline exists. No local change made.'
        return
    }

    $baseline = Get-Content $BaselineFile -Raw | ConvertFrom-Json
    $computerProperty = $baseline.PSObject.Properties['ComputerName']
    if ($computerProperty -and $computerProperty.Value -and ([string]$computerProperty.Value -ne $env:COMPUTERNAME)) {
        throw "Refusing to restore baseline captured for '$($computerProperty.Value)' on '$env:COMPUTERNAME'."
    }

    $entries = @()
    $entriesProperty = $baseline.PSObject.Properties['Entries']

    if ($entriesProperty) {
        $entries = @($entriesProperty.Value)
    }
    else {
        # Backward compatibility for baselines created by the earlier engine.
        # The old property was named "Sids", but secedit can export account-name
        # entries as well. Preserve names as names and add '*' only to real SIDs.
        $legacySidsProperty = $baseline.PSObject.Properties['Sids']
        if ($legacySidsProperty) {
            foreach ($item in @($legacySidsProperty.Value)) {
                $value = ([string]$item).Trim()
                if ([string]::IsNullOrWhiteSpace($value)) { continue }

                if ($value.StartsWith('*')) {
                    $entries += $value
                }
                elseif ($value -match '^S-d-(?:d+-)*d+$') {
                    $entries += "*$value"
                }
                else {
                    $entries += $value
                }
            }
        }
    }

    if ($entries.Count -eq 0) {
        throw 'Baseline entry list is empty. Refusing to restore.'
    }

    Apply-SeInteractiveLogonEntries -Entries $entries
    Write-Log 'Restored and verified baseline SeInteractiveLogonRight.'
}

function Load-Cache {
    if (-not (Test-Path $CacheFile)) { return $null }
    try { $cache = Get-Content $CacheFile -Raw | ConvertFrom-Json }
    catch { throw "Local policy cache is unreadable: $($_.Exception.Message)" }

    $computerProperty = $cache.PSObject.Properties['ComputerName']
    if ($computerProperty -and $computerProperty.Value -and ([string]$computerProperty.Value -ne $env:COMPUTERNAME)) {
        throw "Local policy cache belongs to '$($computerProperty.Value)', not '$env:COMPUTERNAME'."
    }
    return $cache
}

function Get-CachedPrincipalForDn {
    param(
        $Cache,
        [Parameter(Mandatory)][string]$DistinguishedName
    )

    if ($null -eq $Cache) { return $null }

    foreach ($principal in @($Cache.PermanentPrincipals)) {
        if ($null -eq $principal) { continue }

        $dnProperty = $principal.PSObject.Properties['DistinguishedName']
        $sidProperty = $principal.PSObject.Properties['Sid']
        if (-not $dnProperty -or -not $sidProperty) { continue }

        $cachedDn = [string]$dnProperty.Value
        $cachedSid = [string]$sidProperty.Value
        if (
            -not [string]::IsNullOrWhiteSpace($cachedDn) -and
            -not [string]::IsNullOrWhiteSpace($cachedSid) -and
            $cachedDn.Equals($DistinguishedName,[System.StringComparison]::OrdinalIgnoreCase)
        ) {
            return $principal
        }
    }

    return $null
}

function Get-EffectiveSidsFromCache {
    param([Parameter(Mandatory)]$Cache)
    $sids = @($AdministratorsSid)
    foreach ($p in @($Cache.PermanentPrincipals)) {
        if ($p.Sid) { $sids += [string]$p.Sid }
    }
    return @($sids | Sort-Object -Unique)
}

function Reconcile-FromCache {
    $cache = Load-Cache
    if (-not $cache) {
        Write-Log 'No local policy cache exists. No local changes made.'
        return
    }

    if (-not [bool]$cache.PolicyEnabled) {
        Write-Log 'Cached policy is disabled. Techaptic is not managing SeInteractiveLogonRight.'
        return
    }

    Apply-SeInteractiveLogonRight -Sids (Get-EffectiveSidsFromCache -Cache $cache)
}

function Get-NextHalfHourSlot {
    $now = Get-Date
    $hour = $now.Date.AddHours($now.Hour)
    if ($now.Minute -lt 30) { return $hour.AddMinutes(30) }
    return $hour.AddHours(1)
}

function Get-TaskPrincipal {
    New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
}

function Get-TaskSettings {
    New-ScheduledTaskSettingsSet `
        -StartWhenAvailable `
        -AllowStartIfOnBatteries `
        -DontStopIfGoingOnBatteries `
        -MultipleInstances IgnoreNew `
        -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
}

function Register-SyncTask {
    $next = Get-NextHalfHourSlot
    $repeat = New-ScheduledTaskTrigger -Once -At $next -RepetitionInterval (New-TimeSpan -Minutes $SyncMinutes) -RepetitionDuration (New-TimeSpan -Days 3650)
    $startup = New-ScheduledTaskTrigger -AtStartup
    $ps = "$env:SystemRootSystem32WindowsPowerShellv1.0powershell.exe"
    $action = New-ScheduledTaskAction -Execute $ps -Argument "-NoProfile -NonInteractive -File `"$LocalScript`" -Mode Sync"

    Register-ScheduledTask `
        -TaskName $SyncTaskName `
        -Action $action `
        -Trigger @($startup,$repeat) `
        -Principal (Get-TaskPrincipal) `
        -Settings (Get-TaskSettings) `
        -Description "Techaptic assigned-workstation policy sync. Startup + :00/:30." `
        -Force | Out-Null

    Write-Log ("Registered policy sync task. Next clock-aligned sync: {0}" -f $next)
}

function Remove-LegacyTemporaryTasks {
    $removed = 0
    foreach ($suffix in @('Start','Warn15','Warn5','Expire','HardLogoff')) {
        $name = $LegacyTempPrefix + $suffix
        if (Get-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue) {
            Unregister-ScheduledTask -TaskName $name -Confirm:$false -ErrorAction SilentlyContinue
            $removed++
        }
    }
    if ($removed -gt 0) {
        Write-Log "Removed $removed legacy temporary-access scheduled task(s)."
    }
}

function Invoke-Sync {
    Save-BaselineIfNeeded
    Remove-LegacyTemporaryTasks

    try {
        $policy = Get-TechapticComputerPolicy
    }
    catch {
        Write-Log ("WARNING: AD policy read failed: {0}. Falling back to the last successful local cache." -f $_.Exception.Message)
        Reconcile-FromCache
        return
    }

    $previousCache = $null
    try {
        $previousCache = Load-Cache
    }
    catch {
        Write-Log ("WARNING: Existing local policy cache could not be read and will not be used for principal fallback: {0}" -f $_.Exception.Message)
    }

    if (-not $policy.Enabled) {
        $wasPreviouslyEnabled = $false
        if ($null -ne $previousCache) {
            $enabledProperty = $previousCache.PSObject.Properties['PolicyEnabled']
            if ($enabledProperty) {
                $wasPreviouslyEnabled = [bool]$enabledProperty.Value
            }
        }

        if ($wasPreviouslyEnabled) {
            Restore-Baseline
            Write-Log 'Policy transitioned to disabled. Baseline restored once.'
        }
        else {
            Write-Log 'Policy is disabled. Techaptic is not managing SeInteractiveLogonRight.'
        }

        $cache = [ordered]@{
            SchemaVersion         = 1
            ComputerName          = $env:COMPUTERNAME
            ComputerDn            = $policy.ComputerDn
            LastSuccessfulSyncUtc = [datetime]::UtcNow.ToString('o')
            PolicyEnabled         = $false
            PermanentPrincipals   = @()
        }
        Write-JsonAtomic -InputObject $cache -Path $CacheFile -Depth 5
        Write-Log "Policy cache updated: $CacheFile"
        return
    }

    $principals = @()
    foreach ($dn in @($policy.PrincipalDns)) {
        try {
            $principal = Get-PrincipalFromDn -DistinguishedName $dn
            $principals += $principal
            Write-Log ("Allowed principal resolved: {0} ({1}) -> {2}" -f $principal.SamAccountName,$principal.ObjectClass,$principal.Sid)
        }
        catch {
            $cachedPrincipal = Get-CachedPrincipalForDn -Cache $previousCache -DistinguishedName $dn
            if ($null -ne $cachedPrincipal) {
                $principals += $cachedPrincipal
                Write-Log ("WARNING: Current principal DN could not be resolved from AD; reusing the cached SID only for the same current DN: {0} -> {1}" -f $dn,[string]$cachedPrincipal.Sid)
            }
            else {
                Write-Log ("WARNING: Current principal DN could not be resolved and no same-DN cached SID exists. The principal will not be applied during this sync: {0}. Error: {1}" -f $dn,$_.Exception.Message)
            }
        }
    }

    if ($principals.Count -eq 0) {
        Write-Log 'WARNING: Policy is enabled with no resolved principals. Only BUILTINAdministrators will retain local interactive logon.'
    }

    $cache = [ordered]@{
        SchemaVersion         = 1
        ComputerName          = $env:COMPUTERNAME
        ComputerDn            = $policy.ComputerDn
        LastSuccessfulSyncUtc = [datetime]::UtcNow.ToString('o')
        PolicyEnabled         = $true
        PermanentPrincipals   = @($principals)
    }

    Write-JsonAtomic -InputObject $cache -Path $CacheFile -Depth 6
    Write-Log "Policy cache updated: $CacheFile"
    Reconcile-FromCache
    Write-Log 'AD policy sync completed successfully.'
}

function Invoke-Rebaseline {
    $policy = Get-TechapticComputerPolicy
    if ($policy.Enabled) {
        throw 'Rebaseline is blocked while Techaptic workstation policy is enabled. Run Rebaseline only while the policy is disabled or not set.'
    }

    $entries = @(Get-SeInteractiveLogonEntries)
    Write-Baseline -Entries $entries -ReplaceExisting
    Write-Log 'Baseline was explicitly re-captured while Techaptic policy was disabled.'
}

function Invoke-Install {
    if ($PSCommandPath -ne $LocalScript) {
        Copy-Item -Path $PSCommandPath -Destination $LocalScript -Force
        Unblock-File -Path $LocalScript -ErrorAction SilentlyContinue
        Write-Log "Installed engine locally to $LocalScript"
    }
    else {
        Write-Log "Engine install/repair invoked from local path."
    }

    Protect-WorkDirectory
    Save-BaselineIfNeeded
    Remove-LegacyTemporaryTasks
    Register-SyncTask
    Invoke-Sync
}

$exitCode = 0
try {
    Assert-Elevated
    Initialize-WorkDirectory
    Protect-WorkDirectory
    Enter-EngineMutex

    switch ($Mode) {
        'Install'    { Invoke-Install }
        'Sync'       { Invoke-Sync }
        'Rebaseline' { Invoke-Rebaseline }
    }
}
catch {
    try { Write-Log ("ERROR: {0}" -f $_.Exception.Message) }
    catch { Write-Error $_.Exception.Message }
    $exitCode = 1
}
finally {
    Exit-EngineMutex
}

exit $exitCode

Yerel Oturum Açma Yönetimi

<#
.SYNOPSIS
Techaptic - Workstation Access Admin Console

.DESCRIPTION
This Techaptic administration console manages workstation sign-in assignments stored in
Active Directory computer objects.

The console works with these custom Active Directory attributes:

- techapticLogonPolicyEnabled
- techapticLogonPrincipals

It provides an interactive interface for checking the current workstation-access state,
enabling or disabling the Techaptic workstation policy, and adding or removing allowed
Active Directory principals.

User and security-group input is resolved automatically to the required Active Directory
distinguished name before it is stored on the computer object. The console accepts users
and security-enabled groups only.

This console changes Active Directory policy data only. It does not push changes directly
to client computers. Endpoints apply the current policy through the Techaptic Workstation
Access Engine at startup or during the scheduled synchronization cycle.

.REQUIREMENTS
- Windows PowerShell with the ActiveDirectory module available.
- Sufficient Active Directory permissions to read and modify the Techaptic custom
  attributes on the target computer objects.
- The Techaptic schema attributes must already exist and be available on the computer class.
- The Techaptic Workstation Access Engine must be deployed to the intended endpoints for
  policy enforcement.

.OPERATION
Status
    Shows whether the Techaptic workstation policy is Enabled, Disabled, or Not set and
    lists the principals currently stored on the selected computer object.

Enable Policy
    Sets techapticLogonPolicyEnabled to TRUE for the selected computer. If no principals
    are configured, the console requires explicit confirmation because the endpoint engine
    will retain only BUILTINAdministrators for local interactive logon.

Disable Policy
    Sets techapticLogonPolicyEnabled to FALSE. The endpoint engine handles the corresponding
    local policy transition during its next successful synchronization.

Add Allowed Principal
    Resolves an Active Directory user or security-enabled group and stores its distinguished
    name in techapticLogonPrincipals.

Remove Allowed Principal
    Removes one selected distinguished name from techapticLogonPrincipals.

Clear All Allowed Principals
    Removes all configured principals from the selected computer object. If the policy is
    enabled, the console warns that only BUILTINAdministrators will remain after endpoint
    synchronization.

.NOTES
Author     : Techaptic
Maintainer : Techaptic
Script     : Workstation Access Admin Console
Execution  : Interactive administrative PowerShell session

IMPORTANT OPERATIONAL NOTES:
- Run the console with an account that has the required Active Directory permissions.
- This console manages Active Directory configuration only; it does not directly modify the
  local "Allow log on locally" setting on endpoints.
- Endpoint changes are applied by the Techaptic Workstation Access Engine at startup or the
  next scheduled synchronization.
- Only Active Directory users and security-enabled groups are accepted as allowed principals.
- Disabling the policy does not delete stored principal assignments from Active Directory;
  they remain stored but inactive until the policy is enabled again.
- Review "Deny log on locally" and any other GPO, MDM, or script that can conflict with local
  interactive logon rights on the managed endpoints.
#>

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2.0

Import-Module ActiveDirectory -ErrorAction Stop

$PolicyAttribute = 'techapticLogonPolicyEnabled'
$PrincipalsAttribute = 'techapticLogonPrincipals'

function Write-Rule {
    param([ConsoleColor]$Color = [ConsoleColor]::DarkGray)
    Write-Host ('-' * 68) -ForegroundColor $Color
}

function Write-Title {
    Clear-Host
    Write-Host ('=' * 68) -ForegroundColor Cyan
    Write-Host '                 TECHAPTIC WORKSTATION ACCESS' -ForegroundColor Cyan
    Write-Host '                         Admin Console' -ForegroundColor DarkCyan
    Write-Host ('=' * 68) -ForegroundColor Cyan
    Write-Host 'Principal = Active Directory user or security group' -ForegroundColor DarkGray
    Write-Host 'AD changes apply on the endpoint at startup or the next :00/:30 sync.' -ForegroundColor DarkGray
    Write-Host 'User/group input is resolved to the required AD distinguished name automatically.' -ForegroundColor DarkGray
    Write-Host 'At an input prompt, press Enter twice to return to the main menu.' -ForegroundColor DarkGray
    Write-Host ''
}

function Write-Section {
    param([Parameter(Mandatory)][string]$Text)
    Write-Host ''
    Write-Host ("[ {0} ]" -f $Text) -ForegroundColor Cyan
}

function Write-Success {
    param([Parameter(Mandatory)][string]$Text)
    Write-Host $Text -ForegroundColor Green
}

function Write-WarningText {
    param([Parameter(Mandatory)][string]$Text)
    Write-Host $Text -ForegroundColor Yellow
}

function Write-ErrorText {
    param([Parameter(Mandatory)][string]$Text)
    Write-Host ("ERROR: {0}" -f $Text) -ForegroundColor Red
}

function Wait-ForMenu {
    Write-Host ''
    Write-Rule
    [void](Read-Host 'Press Enter to return to the main menu')
}

function Escape-LdapFilterValue {
    param([Parameter(Mandatory)][string]$Value)
    $Value.Replace('','5c').Replace('*','2a').Replace('(','28').Replace(')','29').Replace(([char]0).ToString(),'0')
}

function Confirm-DefaultYes {
    param([Parameter(Mandatory)][string]$Prompt)
    while ($true) {
        Write-Host -NoNewline ("{0} [Y/n]: " -f $Prompt) -ForegroundColor Yellow
        $answer = (Read-Host).Trim()
        if ([string]::IsNullOrWhiteSpace($answer) -or $answer -match '^(?i:y|yes)$') { return $true }
        if ($answer -match '^(?i:n|no)$') { return $false }
        Write-WarningText 'Please enter Y or N.'
    }
}

function Confirm-DefaultNo {
    param([Parameter(Mandatory)][string]$Prompt)
    while ($true) {
        Write-Host -NoNewline ("{0} [y/N]: " -f $Prompt) -ForegroundColor Yellow
        $answer = (Read-Host).Trim()
        if ([string]::IsNullOrWhiteSpace($answer) -or $answer -match '^(?i:n|no)$') { return $false }
        if ($answer -match '^(?i:y|yes)$') { return $true }
        Write-WarningText 'Please enter Y or N.'
    }
}

function Request-MainMenu {
    if (Confirm-DefaultYes 'Return to the main menu?') {
        throw [System.OperationCanceledException]::new('ReturnToMainMenu')
    }
}

function Read-InputOrBack {
    param([Parameter(Mandatory)][string]$Prompt)
    while ($true) {
        Write-Host -NoNewline ("{0}: " -f $Prompt) -ForegroundColor Yellow
        $value = (Read-Host).Trim()
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value }
        Request-MainMenu
    }
}

function Resolve-Computer {
    param([Parameter(Mandatory)][string]$InputValue)
    $escaped = Escape-LdapFilterValue $InputValue.Trim().TrimEnd('$')
    $matches = @(Get-ADComputer -LDAPFilter "(|(name=$escaped)(sAMAccountName=$escaped`$))" -Properties $PolicyAttribute,$PrincipalsAttribute)
    if ($matches.Count -eq 0) { throw "Computer '$InputValue' was not found." }
    if ($matches.Count -gt 1) { throw "Computer '$InputValue' is ambiguous." }
    return $matches[0]
}

function Resolve-Principal {
    param([Parameter(Mandatory)][string]$InputValue)
    $escaped = Escape-LdapFilterValue $InputValue.Trim()
    $filter = "(|(&(objectCategory=person)(objectClass=user)(|(sAMAccountName=$escaped)(userPrincipalName=$escaped)(displayName=$escaped)(name=$escaped)))(&(objectCategory=group)(groupType:1.2.840.113556.1.4.803:=2147483648)(|(sAMAccountName=$escaped)(displayName=$escaped)(name=$escaped))))"
    $matches = @(Get-ADObject -LDAPFilter $filter -Properties sAMAccountName,displayName,objectClass)

    if ($matches.Count -eq 0) { throw "User or security group '$InputValue' was not found." }
    if ($matches.Count -eq 1) { return $matches[0] }

    Write-Section 'MULTIPLE MATCHES'
    for ($i = 0; $i -lt $matches.Count; $i++) {
        Write-Host ('[{0}] {1} [{2}] ({3})' -f ($i + 1),$matches[$i].Name,$matches[$i].ObjectClass,$matches[$i].sAMAccountName)
        Write-Host ('    {0}' -f $matches[$i].DistinguishedName) -ForegroundColor DarkGray
    }

    while ($true) {
        $choice = Read-InputOrBack 'Select a number'
        $number = 0
        if ([int]::TryParse($choice,[ref]$number) -and $number -ge 1 -and $number -le $matches.Count) {
            return $matches[$number - 1]
        }
        Write-WarningText 'Invalid selection.'
    }
}

function Get-ConfiguredDns {
    param([Parameter(Mandatory)]$Computer)
    $value = $Computer.$PrincipalsAttribute
    if ($null -eq $value) { return @() }

    $result = New-Object System.Collections.Generic.List[string]
    foreach ($item in @($value)) {
        if ($null -ne $item) {
            $text = [string]$item
            if (-not [string]::IsNullOrWhiteSpace($text)) { $result.Add($text) }
        }
    }
    return @($result.ToArray())
}

function Get-FreshComputer {
    param([Parameter(Mandatory)]$Computer)
    return Get-ADComputer -Identity $Computer.DistinguishedName -Properties $PolicyAttribute,$PrincipalsAttribute
}

function Show-ComputerState {
    param([Parameter(Mandatory)]$Computer)
    $fresh = Get-FreshComputer $Computer
    $rawPolicy = $fresh.$PolicyAttribute
    $policyState = if ($null -eq $rawPolicy) { 'Not set' } elseif ([bool]$rawPolicy) { 'Enabled' } else { 'Disabled' }

    Write-Section 'CURRENT WORKSTATION ACCESS'
    Write-Host -NoNewline 'Computer : ' -ForegroundColor DarkGray
    Write-Host $fresh.Name -ForegroundColor White
    Write-Host -NoNewline 'Policy   : ' -ForegroundColor DarkGray

    switch ($policyState) {
        'Enabled'        { Write-Host 'Enabled' -ForegroundColor Green }
        'Disabled'       { Write-Host 'Disabled' -ForegroundColor Red }
        'Not set'        { Write-Host 'Not set' -ForegroundColor Yellow }
    }

    Write-Host 'Configured principals:' -ForegroundColor DarkGray
    $dns = @(Get-ConfiguredDns $fresh)

    if ($policyState -ne 'Enabled' -and $dns.Count -gt 0) {
        Write-Host '  Note: these assignments are stored in AD but are inactive while the policy is not enabled.' -ForegroundColor Yellow
    }

    if ($dns.Count -eq 0) {
        Write-WarningText '  (none)'
        if ($policyState -eq 'Enabled') {
            Write-WarningText '  Policy is enabled: only BUILTINAdministrators will retain local interactive logon.'
        }
        return
    }

    foreach ($dn in $dns) {
        try {
            $obj = Get-ADObject -Identity $dn -Properties sAMAccountName,objectClass
            $type = if ($obj.ObjectClass -eq 'group') { 'security group' } else { 'user' }
            Write-Host ('  - {0} [{1}] ({2})' -f $obj.Name,$type,$obj.sAMAccountName) -ForegroundColor White
        }
        catch {
            Write-Host ('  - UNRESOLVED: {0}' -f $dn) -ForegroundColor Red
        }
    }
}

function Select-Computer {
    $name = Read-InputOrBack 'Computer name'
    return Resolve-Computer $name
}

function Add-Principal {
    Write-Section 'ADD ALLOWED PRINCIPAL'
    $computer = Select-Computer
    $inputValue = Read-InputOrBack 'AD user or security group (name, sAMAccountName, UPN, or display name)'
    $principal = Resolve-Principal $inputValue

    $fresh = Get-FreshComputer $computer
    $current = @(Get-ConfiguredDns $fresh)
    $principalDn = [string]$principal.DistinguishedName

    if ($current -contains $principalDn) {
        Write-WarningText 'That principal is already configured.'
        Show-ComputerState $computer
        return
    }

    Set-ADComputer -Identity $computer.DistinguishedName -Add @{ $PrincipalsAttribute = $principalDn }
    Write-Success ("Added '{0}' to '{1}'." -f $principal.Name,$computer.Name)
    Show-ComputerState $computer
}

function Remove-Principal {
    Write-Section 'REMOVE ALLOWED PRINCIPAL'
    $computer = Select-Computer
    $fresh = Get-FreshComputer $computer
    $current = @(Get-ConfiguredDns $fresh)

    if ($current.Count -eq 0) {
        Write-WarningText 'No configured principals exist.'
        return
    }

    Write-Host ''
    for ($i = 0; $i -lt $current.Count; $i++) {
        try {
            $obj = Get-ADObject -Identity $current[$i] -Properties sAMAccountName,objectClass
            $type = if ($obj.ObjectClass -eq 'group') { 'security group' } else { 'user' }
            $label = '{0} [{1}] ({2})' -f $obj.Name,$type,$obj.sAMAccountName
        }
        catch { $label = 'UNRESOLVED: {0}' -f $current[$i] }
        Write-Host ('[{0}] {1}' -f ($i + 1),$label)
    }

    while ($true) {
        $choice = Read-InputOrBack 'Principal number to remove'
        $number = 0
        if ([int]::TryParse($choice,[ref]$number) -and $number -ge 1 -and $number -le $current.Count) { break }
        Write-WarningText 'Invalid selection.'
    }

    $dnToRemove = [string]$current[$number - 1]
    Set-ADComputer -Identity $computer.DistinguishedName -Remove @{ $PrincipalsAttribute = $dnToRemove }
    Write-Success 'Principal removed.'
    Show-ComputerState $computer
}

function Set-PolicyState {
    param([Parameter(Mandatory)][bool]$Enabled)
    Write-Section $(if ($Enabled) { 'ENABLE POLICY' } else { 'DISABLE POLICY' })
    $computer = Select-Computer

    if ($Enabled) {
        $fresh = Get-FreshComputer $computer
        $current = @(Get-ConfiguredDns $fresh)
        if ($current.Count -eq 0) {
            Write-WarningText 'No allowed principals are configured.'
            Write-WarningText 'If enabled, only BUILTINAdministrators will retain local interactive logon.'
            $confirm = Read-InputOrBack 'Type ENABLE to continue'
            if ($confirm -cne 'ENABLE') {
                Write-WarningText 'Policy was not enabled.'
                return
            }
        }
    }

    Set-ADComputer -Identity $computer.DistinguishedName -Replace @{ $PolicyAttribute = $Enabled }
    Write-Success ("Policy {0} for '{1}'." -f $(if ($Enabled) { 'enabled' } else { 'disabled' }),$computer.Name)
    Show-ComputerState $computer
}

function Clear-Principals {
    Write-Section 'CLEAR ALL ALLOWED PRINCIPALS'
    $computer = Select-Computer
    $fresh = Get-FreshComputer $computer
    $enabled = if ($null -eq $fresh.$PolicyAttribute) { $false } else { [bool]$fresh.$PolicyAttribute }

    if (@(Get-ConfiguredDns $fresh).Count -eq 0) {
        Write-WarningText 'No configured principals exist.'
        return
    }

    Write-WarningText ("This will remove every configured principal from '{0}'." -f $computer.Name)
    if ($enabled) {
        Write-WarningText 'Policy is enabled: after endpoint sync, only BUILTINAdministrators will retain local interactive logon.'
    }

    $confirm = Read-InputOrBack 'Type YES to continue'
    if ($confirm -cne 'YES') {
        Write-WarningText 'No principals were removed.'
        return
    }

    Set-ADComputer -Identity $computer.DistinguishedName -Clear $PrincipalsAttribute
    Write-Success 'All configured principals were cleared.'
    Show-ComputerState $computer
}

function Show-Menu {
    Write-Title
    Write-Host 'STATUS' -ForegroundColor DarkCyan
    Write-Host '  1  Show workstation access'
    Write-Host ''
    Write-Host 'POLICY' -ForegroundColor DarkCyan
    Write-Host '  2  Enable policy'
    Write-Host '  3  Disable policy'
    Write-Host ''
    Write-Host 'ASSIGNMENTS' -ForegroundColor DarkCyan
    Write-Host '  4  Add allowed principal (user/security group)'
    Write-Host '  5  Remove allowed principal'
    Write-Host '  6  Clear all allowed principals'
    Write-Host ''
    Write-Rule
    Write-Host '  0  Exit'
    Write-Rule
}

while ($true) {
    Show-Menu
    Write-Host -NoNewline 'Select: ' -ForegroundColor Yellow
    $selection = (Read-Host).Trim()

    if ($selection -eq '0') {
        if (Confirm-DefaultNo 'Exit Techaptic Workstation Access?') {
            Write-Host 'Exiting Techaptic Workstation Access.' -ForegroundColor DarkGray
            return
        }
        continue
    }

    if ([string]::IsNullOrWhiteSpace($selection)) { continue }

    $skipPause = $false
    try {
        switch ($selection) {
            '1' { Write-Section 'SHOW WORKSTATION ACCESS'; Show-ComputerState (Select-Computer) }
            '2' { Set-PolicyState -Enabled $true }
            '3' { Set-PolicyState -Enabled $false }
            '4' { Add-Principal }
            '5' { Remove-Principal }
            '6' { Clear-Principals }
            default { Write-WarningText 'Invalid selection.' }
        }
    }
    catch [System.OperationCanceledException] { $skipPause = $true }
    catch { Write-ErrorText $_.Exception.Message }

    if (-not $skipPause) { Wait-ForMenu }
}

Mini Sınav


QUIZ-TR-Ozel-Cozum-Active-Directory-Istemci-Oturumlari-Bolum-1-Politika-Tabanli-Erisim-Otomasyonu-5
  • Soru
  • Soru
  • Soru
  • Soru
  • Soru

Tam Transkript

Değerlendirme

Post Rating (TR)
Yukarı Çık