Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5f526e244 | ||
|
|
b3e40fabb5 | ||
|
|
a850b56a02 | ||
|
|
ac531db05e | ||
|
|
c8572eb8d4 | ||
|
|
57572c5567 | ||
|
|
1d7c312a67 |
@@ -8,6 +8,9 @@ The repository starts from the current
|
|||||||
source and adds an SGU-specific provider, an mTLS-protected broker, Active
|
source and adds an SGU-specific provider, an mTLS-protected broker, Active
|
||||||
Directory synchronization, deployment scripts, and tests.
|
Directory synchronization, deployment scripts, and tests.
|
||||||
|
|
||||||
|
Ready-to-run bootstrap packages are published on the
|
||||||
|
[releases page](https://github.lci.ulsa.mx/alexrg/SGU-CredentialProvider/releases).
|
||||||
|
|
||||||
## Authentication contract
|
## Authentication contract
|
||||||
|
|
||||||
1. The Windows tile collects a `DO`, `AL`, or `AD` institutional key and a password.
|
1. The Windows tile collects a `DO`, `AL`, or `AD` institutional key and a password.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -85,7 +85,10 @@ The generic SGU credential is rendered as a dedicated branded tile instead of
|
|||||||
being grouped below the anonymous **Other user** tile. Machine policy assigns
|
being grouped below the anonymous **Other user** tile. Machine policy assigns
|
||||||
the SGU CLSID as the default provider, hides the last signed-in identity, and
|
the SGU CLSID as the default provider, hides the last signed-in identity, and
|
||||||
disables local-user enumeration while retaining the built-in Microsoft password
|
disables local-user enumeration while retaining the built-in Microsoft password
|
||||||
provider and its **Other user** recovery path. It enumerates one
|
provider and its **Other user** recovery path. The computer GPO also applies
|
||||||
|
Windows' native default account picture to named Windows accounts; client
|
||||||
|
enrollment installs the La Salle mascot bitmap in Windows' standard account-picture
|
||||||
|
location before that GPO takes effect. It enumerates one
|
||||||
`CPFT_TILE_IMAGE` and places the `CPFT_LARGE_TEXT` heading immediately after it
|
`CPFT_TILE_IMAGE` and places the `CPFT_LARGE_TEXT` heading immediately after it
|
||||||
with `CPFS_DISPLAY_IN_SELECTED_TILE`. LogonUI owns field typography and vertical
|
with `CPFS_DISPLAY_IN_SELECTED_TILE`. LogonUI owns field typography and vertical
|
||||||
tile order: on Windows 10 and 11, the account-name title used by **Other user**
|
tile order: on Windows 10 and 11, the account-name title used by **Other user**
|
||||||
|
|||||||
@@ -52,10 +52,17 @@ El proceso crea o configura de forma idempotente:
|
|||||||
- GPO de experiencia del equipo y restricciones de sesión SGU;
|
- GPO de experiencia del equipo y restricciones de sesión SGU;
|
||||||
- certificado de servidor no exportable y broker mTLS en TCP 8443;
|
- certificado de servidor no exportable y broker mTLS en TCP 8443;
|
||||||
- recurso `\\SERVIDOR\Packages`, con lectura para Domain Computers;
|
- recurso `\\SERVIDOR\Packages`, con lectura para Domain Computers;
|
||||||
- RDP con NLA, WinRM/PowerShell Remoting y reglas administrativas sólo en el
|
- RDP con NLA, WinRM/PowerShell Remoting y reglas administrativas limitadas a
|
||||||
perfil Domain;
|
la subred privada indicada, incluso si Windows tarda en reconocer el perfil
|
||||||
|
Domain después de la promoción;
|
||||||
- pantalla, suspensión e hibernación en Nunca.
|
- pantalla, suspensión e hibernación en Nunca.
|
||||||
|
|
||||||
|
En un servidor con dos NIC, el bootstrap desactiva el registro DNS de la NIC de
|
||||||
|
Internet y obliga al servicio DNS a escuchar y publicar únicamente la IP fija
|
||||||
|
privada. También vuelve a iniciar brevemente esa NIC privada si Windows Server
|
||||||
|
2025 todavía la clasifica como Public al terminar la promoción. La salida HTTPS
|
||||||
|
continúa por la NIC que tenga el gateway predeterminado.
|
||||||
|
|
||||||
El broker arranca con una lista de clientes vacía. Eso no abre el servicio: mTLS
|
El broker arranca con una lista de clientes vacía. Eso no abre el servicio: mTLS
|
||||||
rechaza todos los certificados hasta que el primer cliente registra el suyo.
|
rechaza todos los certificados hasta que el primer cliente registra el suyo.
|
||||||
Los archivos opcionales colocados en `payload\server-content\Packages` al crear
|
Los archivos opcionales colocados en `payload\server-content\Packages` al crear
|
||||||
@@ -129,8 +136,8 @@ validaciones.
|
|||||||
Desde el repositorio y con el SDK fijado en `global.json`:
|
Desde el repositorio y con el SDK fijado en `global.json`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\scripts\New-SguBootstrapPackages.ps1 -Version 0.1.0
|
.\scripts\New-SguBootstrapPackages.ps1 -Version 0.1.1
|
||||||
.\scripts\Publish-GiteaRelease.ps1 -Version 0.1.0
|
.\scripts\Publish-GiteaRelease.ps1 -Version 0.1.1
|
||||||
```
|
```
|
||||||
|
|
||||||
El segundo comando usa `GITEA_TOKEN` sólo en memoria o, si no está definido,
|
El segundo comando usa `GITEA_TOKEN` sólo en memoria o, si no está definido,
|
||||||
@@ -139,7 +146,7 @@ la línea de comandos. Para empaquetar recursos institucionales adicionales:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\scripts\New-SguBootstrapPackages.ps1 `
|
.\scripts\New-SguBootstrapPackages.ps1 `
|
||||||
-Version 0.1.0 `
|
-Version 0.1.1 `
|
||||||
-ServerContentPath C:\Preparacion\Packages
|
-ServerContentPath C:\Preparacion\Packages
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ param(
|
|||||||
[int]$NtlmTimeoutSeconds = 20,
|
[int]$NtlmTimeoutSeconds = 20,
|
||||||
[ValidateRange(2, 90)]
|
[ValidateRange(2, 90)]
|
||||||
[int]$ProfileTimeoutSeconds = 90,
|
[int]$ProfileTimeoutSeconds = 90,
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[string[]]$FirewallRemoteAddress = @('LocalSubnet'),
|
||||||
|
[ipaddress]$FirewallLocalAddress,
|
||||||
[switch]$CreateMissingOus,
|
[switch]$CreateMissingOus,
|
||||||
[switch]$DisableCertificateRevocationCheckForLab
|
[switch]$DisableCertificateRevocationCheckForLab
|
||||||
)
|
)
|
||||||
@@ -215,9 +218,32 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install the SGU Authentication Broker
|
|||||||
throw 'Could not enable recovery for non-crash SGUAuthBroker failures.'
|
throw 'Could not enable recovery for non-crash SGUAuthBroker failures.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Get-NetFirewallRule -DisplayName 'SGU Authentication Broker (mTLS)' -ErrorAction SilentlyContinue)) {
|
$firewallRule = Get-NetFirewallRule `
|
||||||
New-NetFirewallRule -DisplayName 'SGU Authentication Broker (mTLS)' `
|
-DisplayName 'SGU Authentication Broker (mTLS)' `
|
||||||
-Direction Inbound -Action Allow -Protocol TCP -LocalPort 8443 -Profile Domain | Out-Null
|
-ErrorAction SilentlyContinue
|
||||||
|
if (-not $firewallRule) {
|
||||||
|
$firewallParameters = @{
|
||||||
|
DisplayName = 'SGU Authentication Broker (mTLS)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'TCP'
|
||||||
|
LocalPort = 8443
|
||||||
|
Profile = 'Any'
|
||||||
|
RemoteAddress = $FirewallRemoteAddress
|
||||||
|
}
|
||||||
|
if ($FirewallLocalAddress) {
|
||||||
|
$firewallParameters.LocalAddress = $FirewallLocalAddress.IPAddressToString
|
||||||
|
}
|
||||||
|
$firewallRule = New-NetFirewallRule @firewallParameters
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$firewallRule | Set-NetFirewallRule -Enabled True -Profile Any
|
||||||
|
$addressParameters = @{ RemoteAddress = $FirewallRemoteAddress }
|
||||||
|
if ($FirewallLocalAddress) {
|
||||||
|
$addressParameters.LocalAddress = $FirewallLocalAddress.IPAddressToString
|
||||||
|
}
|
||||||
|
$firewallRule | Get-NetFirewallAddressFilter |
|
||||||
|
Set-NetFirewallAddressFilter @addressParameters | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
Start-Service -Name $serviceName
|
Start-Service -Name $serviceName
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ $remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-
|
|||||||
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
|
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesktopPrincipal access")) {
|
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesktopPrincipal access")) {
|
||||||
|
function Invoke-PowerCfgBestEffort {
|
||||||
|
param([Parameter(Mandatory)][string[]]$Arguments)
|
||||||
|
|
||||||
|
# Start-Process keeps powercfg's policy-override diagnostic on its own
|
||||||
|
# stderr stream. In PowerShell 7, directly invoking that native command
|
||||||
|
# turns stderr into a terminating ErrorRecord under $ErrorActionPreference
|
||||||
|
# = 'Stop', which previously aborted this unrelated remediation work.
|
||||||
|
$process = Start-Process -FilePath "$env:SystemRoot\System32\powercfg.exe" `
|
||||||
|
-ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
|
||||||
|
if ($process.ExitCode -ne 0) {
|
||||||
|
Write-Warning "powercfg $($Arguments -join ' ') returned exit code $($process.ExitCode); continuing enrollment repair."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($powerChange in @(
|
foreach ($powerChange in @(
|
||||||
@('monitor-timeout-ac', '0'),
|
@('monitor-timeout-ac', '0'),
|
||||||
@('monitor-timeout-dc', '0'),
|
@('monitor-timeout-dc', '0'),
|
||||||
@@ -27,15 +41,9 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesk
|
|||||||
@('standby-timeout-dc', '0'),
|
@('standby-timeout-dc', '0'),
|
||||||
@('hibernate-timeout-ac', '0'),
|
@('hibernate-timeout-ac', '0'),
|
||||||
@('hibernate-timeout-dc', '0'))) {
|
@('hibernate-timeout-dc', '0'))) {
|
||||||
& powercfg.exe /change $powerChange[0] $powerChange[1]
|
Invoke-PowerCfgBestEffort -Arguments @('/change', $powerChange[0], $powerChange[1])
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "powercfg /change $($powerChange[0]) failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& powercfg.exe /hibernate off
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "powercfg /hibernate off failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
}
|
||||||
|
Invoke-PowerCfgBestEffort -Arguments @('/hibernate', 'off')
|
||||||
|
|
||||||
Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
|
Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
|
||||||
-Name fDenyTSConnections -Type DWord -Value 0
|
-Name fDenyTSConnections -Type DWord -Value 0
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
[CmdletBinding(SupportsShouldProcess)]
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
param()
|
param(
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[string[]]$AllowedRemoteAddress = @('LocalSubnet')
|
||||||
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
@@ -38,9 +41,12 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
|
|||||||
|
|
||||||
Set-Service -Name TermService -StartupType Automatic
|
Set-Service -Name TermService -StartupType Automatic
|
||||||
Start-Service -Name TermService
|
Start-Service -Name TermService
|
||||||
Get-NetFirewallRule -Name 'RemoteDesktop-UserMode-In-TCP','RemoteDesktop-UserMode-In-UDP' `
|
$remoteDesktopRules = @(Get-NetFirewallRule `
|
||||||
-ErrorAction SilentlyContinue |
|
-Name 'RemoteDesktop-UserMode-In-TCP','RemoteDesktop-UserMode-In-UDP' `
|
||||||
Set-NetFirewallRule -Enabled True -Profile Domain
|
-ErrorAction SilentlyContinue)
|
||||||
|
$remoteDesktopRules | Set-NetFirewallRule -Enabled True -Profile Any
|
||||||
|
$remoteDesktopRules | Get-NetFirewallAddressFilter |
|
||||||
|
Set-NetFirewallAddressFilter -RemoteAddress $AllowedRemoteAddress | Out-Null
|
||||||
|
|
||||||
$enableRemoting = Start-Process `
|
$enableRemoting = Start-Process `
|
||||||
-FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
|
-FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
|
||||||
@@ -57,9 +63,12 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
|
|||||||
|
|
||||||
Set-Service -Name WinRM -StartupType Automatic
|
Set-Service -Name WinRM -StartupType Automatic
|
||||||
Start-Service -Name WinRM
|
Start-Service -Name WinRM
|
||||||
Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP','WINRM-HTTP-In-TCP-NoScope' `
|
$winRmRules = @(Get-NetFirewallRule `
|
||||||
-ErrorAction SilentlyContinue |
|
-Name 'WINRM-HTTP-In-TCP','WINRM-HTTP-In-TCP-NoScope' `
|
||||||
Set-NetFirewallRule -Enabled True -Profile Domain
|
-ErrorAction SilentlyContinue)
|
||||||
|
$winRmRules | Set-NetFirewallRule -Enabled True -Profile Any
|
||||||
|
$winRmRules | Get-NetFirewallAddressFilter |
|
||||||
|
Set-NetFirewallAddressFilter -RemoteAddress $AllowedRemoteAddress | Out-Null
|
||||||
Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP-PUBLIC' -ErrorAction SilentlyContinue |
|
Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP-PUBLIC' -ErrorAction SilentlyContinue |
|
||||||
Disable-NetFirewallRule
|
Disable-NetFirewallRule
|
||||||
|
|
||||||
@@ -74,8 +83,11 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
|
|||||||
'WMI-WINMGMT-In-TCP',
|
'WMI-WINMGMT-In-TCP',
|
||||||
'WMI-ASYNC-In-TCP'
|
'WMI-ASYNC-In-TCP'
|
||||||
)
|
)
|
||||||
Get-NetFirewallRule -Name $administrativeRules -ErrorAction SilentlyContinue |
|
$enabledAdministrativeRules = @(Get-NetFirewallRule `
|
||||||
Set-NetFirewallRule -Enabled True -Profile Domain
|
-Name $administrativeRules -ErrorAction SilentlyContinue)
|
||||||
|
$enabledAdministrativeRules | Set-NetFirewallRule -Enabled True -Profile Any
|
||||||
|
$enabledAdministrativeRules | Get-NetFirewallAddressFilter |
|
||||||
|
Set-NetFirewallAddressFilter -RemoteAddress $AllowedRemoteAddress | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
@@ -88,7 +100,8 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable secure administrative RDP
|
|||||||
-Name UserAuthentication) -eq 1
|
-Name UserAuthentication) -eq 1
|
||||||
TermService = (Get-Service TermService).Status
|
TermService = (Get-Service TermService).Status
|
||||||
WinRM = (Get-Service WinRM).Status
|
WinRM = (Get-Service WinRM).Status
|
||||||
FirewallProfile = 'Domain'
|
FirewallProfile = 'Any'
|
||||||
|
AllowedRemoteAddress = $AllowedRemoteAddress
|
||||||
AdministrativeAccessOnly = $true
|
AdministrativeAccessOnly = $true
|
||||||
AlwaysOnPowerPolicyApplied = $true
|
AlwaysOnPowerPolicyApplied = $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,8 +171,10 @@ function Ensure-OrganizationalUnit {
|
|||||||
)
|
)
|
||||||
|
|
||||||
$distinguishedName = "OU=$Name,$Path"
|
$distinguishedName = "OU=$Name,$Path"
|
||||||
$existing = Get-ADOrganizationalUnit -Identity $distinguishedName -Server $Server `
|
$escapedName = $Name.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29')
|
||||||
-ErrorAction SilentlyContinue
|
$existing = Get-ADOrganizationalUnit -LDAPFilter "(ou=$escapedName)" `
|
||||||
|
-SearchBase $Path -SearchScope OneLevel -Server $Server -ErrorAction Stop |
|
||||||
|
Select-Object -First 1
|
||||||
if (-not $existing) {
|
if (-not $existing) {
|
||||||
New-ADOrganizationalUnit -Name $Name -Path $Path `
|
New-ADOrganizationalUnit -Name $Name -Path $Path `
|
||||||
-ProtectedFromAccidentalDeletion $true -Server $Server | Out-Null
|
-ProtectedFromAccidentalDeletion $true -Server $Server | Out-Null
|
||||||
@@ -180,6 +182,57 @@ function Ensure-OrganizationalUnit {
|
|||||||
return $distinguishedName
|
return $distinguishedName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Wait-ActiveDirectoryReady {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$ExpectedBaseDn,
|
||||||
|
[ValidateRange(1, 120)][int]$Attempts = 36,
|
||||||
|
[ValidateRange(1, 30)][int]$DelaySeconds = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
|
||||||
|
try {
|
||||||
|
$rootDse = Get-ADRootDSE -Server localhost -ErrorAction Stop
|
||||||
|
if ($rootDse.DefaultNamingContext -eq $ExpectedBaseDn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if ($attempt -eq $Attempts) {
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds $DelaySeconds
|
||||||
|
}
|
||||||
|
throw "Active Directory did not publish $ExpectedBaseDn before the readiness timeout."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wait-DnsZoneReady {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$ZoneName,
|
||||||
|
[Parameter(Mandatory)][ipaddress]$DnsServer,
|
||||||
|
[ValidateRange(1, 120)][int]$Attempts = 30,
|
||||||
|
[ValidateRange(1, 30)][int]$DelaySeconds = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
|
||||||
|
try {
|
||||||
|
$soa = @(Resolve-DnsName $ZoneName -Type SOA -DnsOnly `
|
||||||
|
-Server $DnsServer.IPAddressToString -ErrorAction Stop |
|
||||||
|
Where-Object Type -eq SOA)
|
||||||
|
if ($soa.Count -gt 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if ($attempt -eq $Attempts) {
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds $DelaySeconds
|
||||||
|
}
|
||||||
|
throw "DNS did not load the $ZoneName zone before the readiness timeout."
|
||||||
|
}
|
||||||
|
|
||||||
function Set-PackageShare {
|
function Set-PackageShare {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$Path,
|
[Parameter(Mandatory)][string]$Path,
|
||||||
@@ -234,6 +287,10 @@ function Set-PackageShare {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Assert-Administrator
|
Assert-Administrator
|
||||||
|
trap {
|
||||||
|
Write-BootstrapLog ("ERROR: " + $_.Exception.Message)
|
||||||
|
throw
|
||||||
|
}
|
||||||
$operatingSystem = Get-CimInstance Win32_OperatingSystem
|
$operatingSystem = Get-CimInstance Win32_OperatingSystem
|
||||||
if ([int]$operatingSystem.ProductType -eq 1) {
|
if ([int]$operatingSystem.ProductType -eq 1) {
|
||||||
throw 'The domain controller bootstrap requires Windows Server, not a Windows client edition.'
|
throw 'The domain controller bootstrap requires Windows Server, not a Windows client edition.'
|
||||||
@@ -379,24 +436,75 @@ if (-not $computer.Domain.Equals($DomainName, [StringComparison]::OrdinalIgnoreC
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-BootstrapLog 'Finalizing Active Directory, DNS, policies, broker, shares, and remote management.'
|
Write-BootstrapLog 'Finalizing Active Directory, DNS, policies, broker, shares, and remote management.'
|
||||||
Import-Module ActiveDirectory -ErrorAction Stop
|
|
||||||
$domain = Get-ADDomain -Identity $DomainName -Server $env:COMPUTERNAME
|
|
||||||
|
|
||||||
$laboratoryOuDn = Ensure-OrganizationalUnit -Name 'Laboratorio' -Path $baseDn -Server $env:COMPUTERNAME
|
# Once the machine is a DC, every active adapter must query the local DNS
|
||||||
$usersOuDn = Ensure-OrganizationalUnit -Name 'Usuarios-SGU' -Path $baseDn -Server $env:COMPUTERNAME
|
# service. Only the private domain adapter may publish its address in the AD
|
||||||
|
# zone; otherwise clients can receive the DHCP/NAT address of the Internet NIC.
|
||||||
|
Get-NetAdapter | Where-Object Status -eq 'Up' | ForEach-Object {
|
||||||
|
Set-DnsClientServerAddress -InterfaceIndex $_.ifIndex `
|
||||||
|
-ServerAddresses $ServerIPv4Address.IPAddressToString
|
||||||
|
Set-DnsClient -InterfaceIndex $_.ifIndex `
|
||||||
|
-RegisterThisConnectionsAddress:($_.Name -eq $NetworkInterfaceAlias)
|
||||||
|
}
|
||||||
|
|
||||||
|
Clear-DnsClientCache
|
||||||
|
Register-DnsClient
|
||||||
|
Import-Module ActiveDirectory -ErrorAction Stop
|
||||||
|
Wait-ActiveDirectoryReady -ExpectedBaseDn $baseDn
|
||||||
|
|
||||||
|
# A newly promoted Windows Server 2025 DC can retain the Public firewall
|
||||||
|
# profile because network identification ran before local DNS and LDAP were
|
||||||
|
# ready. A private-adapter bounce triggers the supported domain-detection path.
|
||||||
|
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if (-not $domainProfile -or $domainProfile.NetworkCategory -ne 'DomainAuthenticated') {
|
||||||
|
Write-BootstrapLog "Refreshing $NetworkInterfaceAlias so Windows detects the domain network profile."
|
||||||
|
Restart-NetAdapter -Name $NetworkInterfaceAlias -Confirm:$false
|
||||||
|
for ($attempt = 1; $attempt -le 15; $attempt++) {
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if ($domainProfile -and $domainProfile.NetworkCategory -eq 'DomainAuthenticated') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply the single-address DNS listener only after any adapter refresh. That
|
||||||
|
# avoids transient DNS socket errors while the private address is momentarily
|
||||||
|
# unavailable, while still preventing the Internet/NAT address from being
|
||||||
|
# published once finalization completes.
|
||||||
|
New-ItemProperty `
|
||||||
|
-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\DNS\Parameters' `
|
||||||
|
-Name PublishAddresses `
|
||||||
|
-PropertyType String `
|
||||||
|
-Value $ServerIPv4Address.IPAddressToString `
|
||||||
|
-Force | Out-Null
|
||||||
|
$dnsServerSetting = Get-DnsServerSetting -All -WarningAction SilentlyContinue
|
||||||
|
$dnsServerSetting.ListeningIPAddress = @($ServerIPv4Address)
|
||||||
|
Set-DnsServerSetting -InputObject $dnsServerSetting -WarningAction SilentlyContinue | Out-Null
|
||||||
|
Restart-Service DNS -Force
|
||||||
|
Wait-DnsZoneReady -ZoneName $DomainName -DnsServer $ServerIPv4Address
|
||||||
|
|
||||||
|
$adServer = 'localhost'
|
||||||
|
$domain = Get-ADDomain -Identity $DomainName -Server $adServer
|
||||||
|
$laboratoryOuDn = Ensure-OrganizationalUnit -Name 'Laboratorio' -Path $baseDn -Server $adServer
|
||||||
|
$usersOuDn = Ensure-OrganizationalUnit -Name 'Usuarios-SGU' -Path $baseDn -Server $adServer
|
||||||
foreach ($ouName in @('Docentes', 'Alumnos', 'Administrativos')) {
|
foreach ($ouName in @('Docentes', 'Alumnos', 'Administrativos')) {
|
||||||
Ensure-OrganizationalUnit -Name $ouName -Path $usersOuDn -Server $env:COMPUTERNAME | Out-Null
|
Ensure-OrganizationalUnit -Name $ouName -Path $usersOuDn -Server $adServer | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
$remoteDesktopGroupName = 'SG-Laboratorio-Usuarios-RDP'
|
$remoteDesktopGroupName = 'SG-Laboratorio-Usuarios-RDP'
|
||||||
$remoteDesktopGroup = Get-ADGroup -Identity $remoteDesktopGroupName -Server $env:COMPUTERNAME `
|
$remoteDesktopGroup = Get-ADGroup -LDAPFilter "(sAMAccountName=$remoteDesktopGroupName)" `
|
||||||
|
-SearchBase $baseDn -SearchScope Subtree -Server $adServer `
|
||||||
-ErrorAction SilentlyContinue
|
-ErrorAction SilentlyContinue
|
||||||
if (-not $remoteDesktopGroup) {
|
if (-not $remoteDesktopGroup) {
|
||||||
New-ADGroup -Name $remoteDesktopGroupName -SamAccountName $remoteDesktopGroupName `
|
New-ADGroup -Name $remoteDesktopGroupName -SamAccountName $remoteDesktopGroupName `
|
||||||
-GroupCategory Security -GroupScope Global -Path $laboratoryOuDn `
|
-GroupCategory Security -GroupScope Global -Path $laboratoryOuDn `
|
||||||
-Description 'SGU users permitted to use Remote Desktop on laboratory clients.' `
|
-Description 'SGU users permitted to use Remote Desktop on laboratory clients.' `
|
||||||
-Server $env:COMPUTERNAME | Out-Null
|
-Server $adServer | Out-Null
|
||||||
$remoteDesktopGroup = Get-ADGroup -Identity $remoteDesktopGroupName -Server $env:COMPUTERNAME
|
$remoteDesktopGroup = Get-ADGroup -LDAPFilter "(sAMAccountName=$remoteDesktopGroupName)" `
|
||||||
|
-SearchBase $laboratoryOuDn -SearchScope OneLevel -Server $adServer
|
||||||
}
|
}
|
||||||
|
|
||||||
& (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1') `
|
& (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1') `
|
||||||
@@ -444,16 +552,30 @@ if (Test-Path -LiteralPath $brokerConfigurationPath -PathType Leaf) {
|
|||||||
-PublishPath $brokerPublishPath `
|
-PublishPath $brokerPublishPath `
|
||||||
-ServerCertificateSubject $brokerDnsName `
|
-ServerCertificateSubject $brokerDnsName `
|
||||||
-AllowedClientThumbprints $allowedClientThumbprints `
|
-AllowedClientThumbprints $allowedClientThumbprints `
|
||||||
-LdapHost $env:COMPUTERNAME `
|
-LdapHost $adServer `
|
||||||
-BaseDn $baseDn `
|
-BaseDn $baseDn `
|
||||||
-DomainNetbios $DomainNetbios `
|
-DomainNetbios $DomainNetbios `
|
||||||
-UpnSuffix $DomainName `
|
-UpnSuffix $DomainName `
|
||||||
-RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName `
|
-RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName `
|
||||||
-DefaultCompany 'La Salle' `
|
-DefaultCompany 'La Salle' `
|
||||||
|
-FirewallLocalAddress $ServerIPv4Address `
|
||||||
|
-FirewallRemoteAddress "$($ServerIPv4Address.IPAddressToString)/$PrefixLength" `
|
||||||
-CreateMissingOus `
|
-CreateMissingOus `
|
||||||
-DisableCertificateRevocationCheckForLab | Out-Null
|
-DisableCertificateRevocationCheckForLab | Out-Null
|
||||||
|
|
||||||
& (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') | Out-Null
|
# Remove stale A records registered by any non-domain/NAT adapter before its
|
||||||
|
# dynamic DNS registration was disabled.
|
||||||
|
$hostRecords = @(Get-DnsServerResourceRecord -ZoneName $DomainName `
|
||||||
|
-Name $env:COMPUTERNAME -RRType A -ErrorAction SilentlyContinue)
|
||||||
|
foreach ($hostRecord in $hostRecords) {
|
||||||
|
if ($hostRecord.RecordData.IPv4Address.IPAddressToString -ne $ServerIPv4Address.IPAddressToString) {
|
||||||
|
Remove-DnsServerResourceRecord -ZoneName $DomainName -InputObject $hostRecord -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateSubnet = "$($ServerIPv4Address.IPAddressToString)/$PrefixLength"
|
||||||
|
& (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') `
|
||||||
|
-AllowedRemoteAddress $privateSubnet | Out-Null
|
||||||
|
|
||||||
$contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages'
|
$contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages'
|
||||||
if (Test-Path -LiteralPath $contentPath -PathType Container) {
|
if (Test-Path -LiteralPath $contentPath -PathType Container) {
|
||||||
@@ -463,6 +585,27 @@ if (Test-Path -LiteralPath $contentPath -PathType Container) {
|
|||||||
Set-PackageShare -Path $PackageSharePath -NetbiosName $DomainNetbios `
|
Set-PackageShare -Path $PackageSharePath -NetbiosName $DomainNetbios `
|
||||||
-DomainSid $domain.DomainSID.Value
|
-DomainSid $domain.DomainSID.Value
|
||||||
|
|
||||||
|
$packageFirewallRule = Get-NetFirewallRule -DisplayName 'SGU Bootstrap Packages (SMB)' `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if (-not $packageFirewallRule) {
|
||||||
|
New-NetFirewallRule `
|
||||||
|
-DisplayName 'SGU Bootstrap Packages (SMB)' `
|
||||||
|
-Direction Inbound `
|
||||||
|
-Action Allow `
|
||||||
|
-Protocol TCP `
|
||||||
|
-LocalPort 445 `
|
||||||
|
-LocalAddress $ServerIPv4Address.IPAddressToString `
|
||||||
|
-RemoteAddress $privateSubnet `
|
||||||
|
-Profile Any | Out-Null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$packageFirewallRule | Set-NetFirewallRule -Enabled True -Profile Any
|
||||||
|
$packageFirewallRule | Get-NetFirewallAddressFilter |
|
||||||
|
Set-NetFirewallAddressFilter `
|
||||||
|
-LocalAddress $ServerIPv4Address.IPAddressToString `
|
||||||
|
-RemoteAddress $privateSubnet | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
& (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1') `
|
& (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1') `
|
||||||
-TargetOuDn $laboratoryOuDn -DomainController $env:COMPUTERNAME | Out-Null
|
-TargetOuDn $laboratoryOuDn -DomainController $env:COMPUTERNAME | Out-Null
|
||||||
$userPolicyParameters = @{
|
$userPolicyParameters = @{
|
||||||
@@ -493,6 +636,8 @@ $validation = [ordered]@{
|
|||||||
LaboratoryOu = $laboratoryOuDn
|
LaboratoryOu = $laboratoryOuDn
|
||||||
UsersOu = $usersOuDn
|
UsersOu = $usersOuDn
|
||||||
RemoteDesktopGroup = $remoteDesktopGroup.DistinguishedName
|
RemoteDesktopGroup = $remoteDesktopGroup.DistinguishedName
|
||||||
|
DomainNetworkProfile = [string](Get-NetConnectionProfile `
|
||||||
|
-InterfaceAlias $NetworkInterfaceAlias -ErrorAction SilentlyContinue).NetworkCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($validation.BrokerService -ne 'Running' -or
|
if ($validation.BrokerService -ne 'Running' -or
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ $providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authent
|
|||||||
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
|
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
|
||||||
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||||
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||||
|
$accountPictureSourcePath = Join-Path $PublishPath 'branding\user.png'
|
||||||
|
$accountPictureDirectory = Join-Path $env:ProgramData 'Microsoft\User Account Pictures'
|
||||||
|
|
||||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||||
@@ -56,6 +58,70 @@ function Test-DotNet10Runtime {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Install-DefaultAccountPicture {
|
||||||
|
param([Parameter(Mandatory)][string]$SourcePath)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $SourcePath -PathType Leaf)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
New-Item -ItemType Directory -Path $accountPictureDirectory -Force | Out-Null
|
||||||
|
|
||||||
|
function Save-AccountPicture {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][Drawing.Image]$Image,
|
||||||
|
[Parameter(Mandatory)][string]$Path,
|
||||||
|
[Parameter(Mandatory)][Drawing.Imaging.ImageFormat]$Format
|
||||||
|
)
|
||||||
|
|
||||||
|
$stream = [IO.MemoryStream]::new()
|
||||||
|
try {
|
||||||
|
$Image.Save($stream, $Format)
|
||||||
|
[IO.File]::WriteAllBytes($Path, $stream.ToArray())
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$stream.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = [Drawing.Image]::FromFile($SourcePath)
|
||||||
|
try {
|
||||||
|
foreach ($size in @(192, 48, 40, 32)) {
|
||||||
|
$bitmap = [Drawing.Bitmap]::new($size, $size)
|
||||||
|
try {
|
||||||
|
$graphics = [Drawing.Graphics]::FromImage($bitmap)
|
||||||
|
try {
|
||||||
|
$graphics.Clear([Drawing.Color]::Transparent)
|
||||||
|
$graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||||
|
$graphics.DrawImage($source, [Drawing.Rectangle]::new(0, 0, $size, $size))
|
||||||
|
Save-AccountPicture -Image $bitmap `
|
||||||
|
-Path (Join-Path $accountPictureDirectory "user-$size.png") `
|
||||||
|
-Format ([Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$graphics.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$bitmap.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Save-AccountPicture -Image $source `
|
||||||
|
-Path (Join-Path $accountPictureDirectory 'user.png') `
|
||||||
|
-Format ([Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
Save-AccountPicture -Image $source `
|
||||||
|
-Path (Join-Path $accountPictureDirectory 'user.bmp') `
|
||||||
|
-Format ([Drawing.Imaging.ImageFormat]::Bmp)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$source.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Test-DotNet10Runtime)) {
|
if (-not (Test-DotNet10Runtime)) {
|
||||||
if (-not $InstallDotNetRuntime) {
|
if (-not $InstallDotNetRuntime) {
|
||||||
throw 'Microsoft .NET 10 x64 runtime is required. Re-run with -InstallDotNetRuntime or install it first.'
|
throw 'Microsoft .NET 10 x64 runtime is required. Re-run with -InstallDotNetRuntime or install it first.'
|
||||||
@@ -166,6 +232,10 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
|
|||||||
[IO.File]::WriteAllText($completeMarker, $packageHash, [Text.UTF8Encoding]::new($false))
|
[IO.File]::WriteAllText($completeMarker, $packageHash, [Text.UTF8Encoding]::new($false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The domain GPO selects the Windows default account picture. Install its
|
||||||
|
# branded bitmap during enrollment so no per-machine manual setup is needed.
|
||||||
|
Install-DefaultAccountPicture -SourcePath $accountPictureSourcePath | Out-Null
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path (Split-Path $settingsPath -Parent) -Force | Out-Null
|
New-Item -ItemType Directory -Path (Split-Path $settingsPath -Parent) -Force | Out-Null
|
||||||
$settingsJson = @{
|
$settingsJson = @{
|
||||||
BrokerEndpoint = $BrokerEndpoint
|
BrokerEndpoint = $BrokerEndpoint
|
||||||
@@ -179,10 +249,14 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
|
|||||||
|
|
||||||
$acl = Get-Acl -LiteralPath (Split-Path $settingsPath -Parent)
|
$acl = Get-Acl -LiteralPath (Split-Path $settingsPath -Parent)
|
||||||
$acl.SetAccessRuleProtection($true, $false)
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
|
# Resolve built-in identities by SID instead of localized display names.
|
||||||
|
# "BUILTIN\Administrators" is not resolvable on every non-English client.
|
||||||
|
$systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18')
|
||||||
|
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
|
||||||
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||||
'SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
$systemSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
||||||
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||||
'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
$administratorsSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
||||||
Set-Acl -LiteralPath (Split-Path $settingsPath -Parent) -AclObject $acl
|
Set-Acl -LiteralPath (Split-Path $settingsPath -Parent) -AclObject $acl
|
||||||
|
|
||||||
New-Item -Path $classRegistryPath -Force | Out-Null
|
New-Item -Path $classRegistryPath -Force | Out-Null
|
||||||
|
|||||||
@@ -71,8 +71,12 @@ if ($PSCmdlet.ShouldProcess($enrollmentRoot, 'Install the SGU enrollment repair
|
|||||||
$runtimeDirectory = Join-Path $enrollmentRoot 'prerequisites'
|
$runtimeDirectory = Join-Path $enrollmentRoot 'prerequisites'
|
||||||
New-Item -ItemType Directory -Path $runtimeDirectory -Force | Out-Null
|
New-Item -ItemType Directory -Path $runtimeDirectory -Force | Out-Null
|
||||||
$guardRuntimeInstaller = Join-Path $runtimeDirectory (Split-Path $DotNetRuntimeInstallerPath -Leaf)
|
$guardRuntimeInstaller = Join-Path $runtimeDirectory (Split-Path $DotNetRuntimeInstallerPath -Leaf)
|
||||||
|
$sourceRuntimeInstaller = [IO.Path]::GetFullPath($DotNetRuntimeInstallerPath)
|
||||||
|
$destinationRuntimeInstaller = [IO.Path]::GetFullPath($guardRuntimeInstaller)
|
||||||
|
if (-not $sourceRuntimeInstaller.Equals($destinationRuntimeInstaller, [StringComparison]::OrdinalIgnoreCase)) {
|
||||||
Copy-Item -LiteralPath $DotNetRuntimeInstallerPath -Destination $guardRuntimeInstaller -Force
|
Copy-Item -LiteralPath $DotNetRuntimeInstallerPath -Destination $guardRuntimeInstaller -Force
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$guardConfiguration = [ordered]@{
|
$guardConfiguration = [ordered]@{
|
||||||
PublishPath = $guardPublishPath
|
PublishPath = $guardPublishPath
|
||||||
@@ -92,10 +96,13 @@ if ($PSCmdlet.ShouldProcess($enrollmentRoot, 'Install the SGU enrollment repair
|
|||||||
|
|
||||||
$acl = Get-Acl -LiteralPath $enrollmentRoot
|
$acl = Get-Acl -LiteralPath $enrollmentRoot
|
||||||
$acl.SetAccessRuleProtection($true, $false)
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
|
# Well-known SIDs are invariant across localized Windows installations.
|
||||||
|
$systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18')
|
||||||
|
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
|
||||||
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||||
'SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
$systemSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
||||||
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||||
'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
$administratorsSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
||||||
Set-Acl -LiteralPath $enrollmentRoot -AclObject $acl
|
Set-Acl -LiteralPath $enrollmentRoot -AclObject $acl
|
||||||
|
|
||||||
$repairScript = Join-Path $enrollmentRoot 'Repair-SguClientEnrollment.ps1'
|
$repairScript = Join-Path $enrollmentRoot 'Repair-SguClientEnrollment.ps1'
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ Copy-Item -Path (Join-Path $providerOutput '*') `
|
|||||||
-Destination (New-Item -ItemType Directory `
|
-Destination (New-Item -ItemType Directory `
|
||||||
-Path (Join-Path $clientRoot 'payload\credential-provider') -Force).FullName `
|
-Path (Join-Path $clientRoot 'payload\credential-provider') -Force).FullName `
|
||||||
-Recurse -Force
|
-Recurse -Force
|
||||||
|
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\lasalle-mascot-account.png') `
|
||||||
|
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\user.png')
|
||||||
Copy-RequiredFile -Source $runtimeInstaller.FullName `
|
Copy-RequiredFile -Source $runtimeInstaller.FullName `
|
||||||
-Destination (Join-Path $clientRoot "payload\prerequisites\$($runtimeInstaller.Name)")
|
-Destination (Join-Path $clientRoot "payload\prerequisites\$($runtimeInstaller.Name)")
|
||||||
Write-PackageManifest -PackageRoot $clientRoot -PackageVersion $Version -PackageKind Client
|
Write-PackageManifest -PackageRoot $clientRoot -PackageVersion $Version -PackageKind Client
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ try {
|
|||||||
$releaseNotes = @"
|
$releaseNotes = @"
|
||||||
Bootstrap reproducible para el laboratorio SGU.
|
Bootstrap reproducible para el laboratorio SGU.
|
||||||
|
|
||||||
|
- **Advertencia:** el bootstrap de servidor crea un bosque nuevo. No restaura los SID, contraseñas ni relaciones de confianza del bosque anterior; para conservarlos se requiere una recuperación de bosque desde una copia de estado del sistema.
|
||||||
- `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio.
|
- `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio.
|
||||||
- `sgu-client-bootstrap-$Version.zip`: registra un certificado mTLS único, instala y valida el Credential Provider antes de unir el equipo al dominio, habilita RDP/WinRM y se repara al arranque.
|
- `sgu-client-bootstrap-$Version.zip`: registra un certificado mTLS único, instala y valida el Credential Provider antes de unir el equipo al dominio, habilita RDP/WinRM y se repara al arranque.
|
||||||
- Windows Home se detecta y se rechaza con una explicación, ya que no admite unión a Active Directory ni RDP host.
|
- Windows Home se detecta y se rechaza con una explicación, ya que no admite unión a Active Directory ni RDP host.
|
||||||
|
|||||||
@@ -8,18 +8,65 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$existing = Get-DnsServerResourceRecord -ZoneName $ZoneName -Name $RecordName -RRType A -ErrorAction SilentlyContinue
|
$dnsReady = $false
|
||||||
if ($existing) {
|
for ($attempt = 1; $attempt -le 30; $attempt++) {
|
||||||
$current = @($existing.RecordData.IPv4Address.IPAddressToString)
|
try {
|
||||||
if ($current.Count -ne 1 -or $current[0] -ne $IPv4Address.IPAddressToString) {
|
$soa = @(Resolve-DnsName $ZoneName -Type SOA -DnsOnly -Server localhost `
|
||||||
# The fixed lab address is an explicit bootstrap input and may change
|
-ErrorAction Stop | Where-Object Type -eq SOA)
|
||||||
# when the server is rebuilt. Replace only this exact A record set.
|
if ($soa.Count -gt 0) {
|
||||||
$existing | Remove-DnsServerResourceRecord -ZoneName $ZoneName -Force
|
$dnsReady = $true
|
||||||
Add-DnsServerResourceRecordA -ZoneName $ZoneName -Name $RecordName -IPv4Address $IPv4Address
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# An AD-integrated zone can take a few seconds to load after DNS starts.
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
}
|
}
|
||||||
else {
|
if (-not $dnsReady) {
|
||||||
Add-DnsServerResourceRecordA -ZoneName $ZoneName -Name $RecordName -IPv4Address $IPv4Address
|
throw "DNS did not load the $ZoneName zone before the readiness timeout."
|
||||||
|
}
|
||||||
|
|
||||||
|
$recordReady = $false
|
||||||
|
for ($attempt = 1; $attempt -le 5; $attempt++) {
|
||||||
|
$existing = @(Get-DnsServerResourceRecord -ZoneName $ZoneName -Name $RecordName `
|
||||||
|
-RRType A -ErrorAction SilentlyContinue)
|
||||||
|
$unwanted = @($existing | Where-Object {
|
||||||
|
$_.RecordData.IPv4Address.IPAddressToString -ne $IPv4Address.IPAddressToString
|
||||||
|
})
|
||||||
|
foreach ($record in $unwanted) {
|
||||||
|
Remove-DnsServerResourceRecord -ZoneName $ZoneName -InputObject $record -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$desired = @($existing | Where-Object {
|
||||||
|
$_.RecordData.IPv4Address.IPAddressToString -eq $IPv4Address.IPAddressToString
|
||||||
|
})
|
||||||
|
if ($desired.Count -eq 0) {
|
||||||
|
try {
|
||||||
|
Add-DnsServerResourceRecordA -ZoneName $ZoneName -Name $RecordName `
|
||||||
|
-IPv4Address $IPv4Address -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# A record that becomes visible while an AD-integrated zone is
|
||||||
|
# finishing its load is harmless; the verified read below decides.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 250
|
||||||
|
$final = @(Get-DnsServerResourceRecord -ZoneName $ZoneName -Name $RecordName `
|
||||||
|
-RRType A -ErrorAction SilentlyContinue)
|
||||||
|
$finalAddresses = @($final | ForEach-Object {
|
||||||
|
$_.RecordData.IPv4Address.IPAddressToString
|
||||||
|
})
|
||||||
|
if ($finalAddresses.Count -eq 1 -and
|
||||||
|
$finalAddresses[0] -eq $IPv4Address.IPAddressToString) {
|
||||||
|
$recordReady = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
if (-not $recordReady) {
|
||||||
|
throw "The $RecordName.$ZoneName A record could not be set exclusively to $IPv4Address."
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($ExternalForwarders.Count -gt 0) {
|
if ($ExternalForwarders.Count -gt 0) {
|
||||||
|
|||||||
@@ -61,15 +61,29 @@ elseif (-not $existingLinkEnabled -and
|
|||||||
|
|
||||||
$dataCollectionKey = 'HKLM\Software\Policies\Microsoft\Windows\DataCollection'
|
$dataCollectionKey = 'HKLM\Software\Policies\Microsoft\Windows\DataCollection'
|
||||||
$powerPolicyRoot = 'HKLM\Software\Policies\Microsoft\Power\PowerSettings'
|
$powerPolicyRoot = 'HKLM\Software\Policies\Microsoft\Power\PowerSettings'
|
||||||
|
$credentialProviderPolicyKey = 'HKLM\Software\Policies\Microsoft\Windows\System'
|
||||||
|
$interactiveLogonPolicyKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||||
|
$accountPicturePolicyKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
$providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
|
||||||
$policies = @(
|
$policies = @(
|
||||||
@{ Key = $dataCollectionKey; Name = 'AllowTelemetry'; Value = 0 },
|
@{ Key = $dataCollectionKey; Name = 'AllowTelemetry'; Type = 'DWord'; Value = 0 },
|
||||||
@{ Key = $dataCollectionKey; Name = 'DisableTelemetryOptInSettingsUx'; Value = 1 },
|
@{ Key = $dataCollectionKey; Name = 'DisableTelemetryOptInSettingsUx'; Type = 'DWord'; Value = 1 },
|
||||||
@{ Key = $dataCollectionKey; Name = 'DisableTelemetryOptInChangeNotification'; Value = 1 },
|
@{ Key = $dataCollectionKey; Name = 'DisableTelemetryOptInChangeNotification'; Type = 'DWord'; Value = 1 },
|
||||||
@{ Key = $dataCollectionKey; Name = 'DisableDiagnosticDataViewer'; Value = 1 },
|
@{ Key = $dataCollectionKey; Name = 'DisableDiagnosticDataViewer'; Type = 'DWord'; Value = 1 },
|
||||||
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\OOBE'; Name = 'DisablePrivacyExperience'; Value = 1 },
|
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\OOBE'; Name = 'DisablePrivacyExperience'; Type = 'DWord'; Value = 1 },
|
||||||
@{ Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System'; Name = 'EnableFirstLogonAnimation'; Value = 0 },
|
@{ Key = $interactiveLogonPolicyKey; Name = 'EnableFirstLogonAnimation'; Type = 'DWord'; Value = 0 },
|
||||||
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\LocationAndSensors'; Name = 'DisableLocation'; Value = 1 },
|
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\LocationAndSensors'; Name = 'DisableLocation'; Type = 'DWord'; Value = 1 },
|
||||||
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\AppPrivacy'; Name = 'LetAppsAccessLocation'; Value = 2 }
|
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\AppPrivacy'; Name = 'LetAppsAccessLocation'; Type = 'DWord'; Value = 2 },
|
||||||
|
|
||||||
|
# Enrollment selects the provider before domain join; this computer GPO
|
||||||
|
# becomes the authoritative, self-healing configuration afterwards.
|
||||||
|
@{ Key = $credentialProviderPolicyKey; Name = 'DefaultCredentialProvider'; Type = 'String'; Value = $providerClassId },
|
||||||
|
@{ Key = $credentialProviderPolicyKey; Name = 'EnumerateLocalUsers'; Type = 'DWord'; Value = 0 },
|
||||||
|
@{ Key = $interactiveLogonPolicyKey; Name = 'DontDisplayLastUserName'; Type = 'DWord'; Value = 1 },
|
||||||
|
|
||||||
|
# Use Windows' native default account image for named user tiles. LogonUI
|
||||||
|
# retains ownership of the anonymous Other user tile and its circular mask.
|
||||||
|
@{ Key = $accountPicturePolicyKey; Name = 'UseDefaultTile'; Type = 'DWord'; Value = 1 }
|
||||||
)
|
)
|
||||||
|
|
||||||
$powerSettingIds = @(
|
$powerSettingIds = @(
|
||||||
@@ -80,8 +94,8 @@ $powerSettingIds = @(
|
|||||||
)
|
)
|
||||||
foreach ($settingId in $powerSettingIds) {
|
foreach ($settingId in $powerSettingIds) {
|
||||||
$settingKey = "$powerPolicyRoot\$settingId"
|
$settingKey = "$powerPolicyRoot\$settingId"
|
||||||
$policies += @{ Key = $settingKey; Name = 'ACSettingIndex'; Value = 0 }
|
$policies += @{ Key = $settingKey; Name = 'ACSettingIndex'; Type = 'DWord'; Value = 0 }
|
||||||
$policies += @{ Key = $settingKey; Name = 'DCSettingIndex'; Value = 0 }
|
$policies += @{ Key = $settingKey; Name = 'DCSettingIndex'; Type = 'DWord'; Value = 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($policy in $policies) {
|
foreach ($policy in $policies) {
|
||||||
@@ -92,7 +106,7 @@ foreach ($policy in $policies) {
|
|||||||
-Server $DomainController `
|
-Server $DomainController `
|
||||||
-Key $policy.Key `
|
-Key $policy.Key `
|
||||||
-ValueName $policy.Name `
|
-ValueName $policy.Name `
|
||||||
-Type DWord `
|
-Type $policy.Type `
|
||||||
-Value $policy.Value | Out-Null
|
-Value $policy.Value | Out-Null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +119,7 @@ foreach ($policy in $policies) {
|
|||||||
-Server $DomainController `
|
-Server $DomainController `
|
||||||
-Key $policy.Key `
|
-Key $policy.Key `
|
||||||
-ValueName $policy.Name
|
-ValueName $policy.Name
|
||||||
$configuredPolicies[$policy.Name + '@' + $policy.Key] = [int]$configured.Value
|
$configuredPolicies[$policy.Name + '@' + $policy.Key] = $configured.Value
|
||||||
}
|
}
|
||||||
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
|
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
|
||||||
Where-Object DisplayName -eq $GpoName |
|
Where-Object DisplayName -eq $GpoName |
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ $ErrorActionPreference = 'Stop'
|
|||||||
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||||
$policyValueName = 'DisableLockWorkstation'
|
$policyValueName = 'DisableLockWorkstation'
|
||||||
$desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
|
$desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
|
||||||
|
$themeKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
|
||||||
|
|
||||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||||
@@ -83,6 +84,19 @@ if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking w
|
|||||||
-Type String `
|
-Type String `
|
||||||
-Value '0' | Out-Null
|
-Value '0' | Out-Null
|
||||||
|
|
||||||
|
# Apply the native Windows dark theme at user logon. Both values are required:
|
||||||
|
# one controls the shell and the other controls supported applications.
|
||||||
|
foreach ($themeValueName in 'AppsUseLightTheme', 'SystemUsesLightTheme') {
|
||||||
|
Set-GPRegistryValue `
|
||||||
|
-Name $GpoName `
|
||||||
|
-Domain $domainName `
|
||||||
|
-Server $DomainController `
|
||||||
|
-Key $themeKey `
|
||||||
|
-ValueName $themeValueName `
|
||||||
|
-Type DWord `
|
||||||
|
-Value 0 | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
if ($WallpaperPath) {
|
if ($WallpaperPath) {
|
||||||
Set-GPRegistryValue `
|
Set-GPRegistryValue `
|
||||||
-Name $GpoName `
|
-Name $GpoName `
|
||||||
@@ -115,6 +129,18 @@ $screenSaverValue = Get-GPRegistryValue `
|
|||||||
-Server $DomainController `
|
-Server $DomainController `
|
||||||
-Key $desktopPolicyKey `
|
-Key $desktopPolicyKey `
|
||||||
-ValueName 'ScreenSaveActive'
|
-ValueName 'ScreenSaveActive'
|
||||||
|
$appsThemeValue = Get-GPRegistryValue `
|
||||||
|
-Name $GpoName `
|
||||||
|
-Domain $domainName `
|
||||||
|
-Server $DomainController `
|
||||||
|
-Key $themeKey `
|
||||||
|
-ValueName 'AppsUseLightTheme'
|
||||||
|
$systemThemeValue = Get-GPRegistryValue `
|
||||||
|
-Name $GpoName `
|
||||||
|
-Domain $domainName `
|
||||||
|
-Server $DomainController `
|
||||||
|
-Key $themeKey `
|
||||||
|
-ValueName 'SystemUsesLightTheme'
|
||||||
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
|
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
|
||||||
Where-Object DisplayName -eq $GpoName |
|
Where-Object DisplayName -eq $GpoName |
|
||||||
Select-Object -First 1
|
Select-Object -First 1
|
||||||
@@ -138,5 +164,6 @@ if ($WallpaperPath) {
|
|||||||
LinkEnabled = [bool]$linkEnabled
|
LinkEnabled = [bool]$linkEnabled
|
||||||
DisableLockWorkstation = [int]$configuredValue.Value
|
DisableLockWorkstation = [int]$configuredValue.Value
|
||||||
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
|
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
|
||||||
|
DarkMode = ([int]$appsThemeValue.Value -eq 0) -and ([int]$systemThemeValue.Value -eq 0)
|
||||||
Wallpaper = $configuredWallpaper
|
Wallpaper = $configuredWallpaper
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
|
||||||
namespace SGU.CredentialProvider;
|
namespace SGU.CredentialProvider;
|
||||||
|
|
||||||
internal static class ProviderTileIcon
|
internal static class ProviderTileIcon
|
||||||
{
|
{
|
||||||
public const int Size = 72;
|
public const int Size = 72;
|
||||||
|
private const string MascotResourceName = "SGU.CredentialProvider.Branding.LaSalleMascot.png";
|
||||||
|
|
||||||
public static Bitmap Create()
|
public static Bitmap Create()
|
||||||
{
|
{
|
||||||
Bitmap bitmap = new(Size, Size, PixelFormat.Format32bppArgb);
|
Bitmap bitmap = new(Size, Size, PixelFormat.Format32bppArgb);
|
||||||
using Graphics graphics = Graphics.FromImage(bitmap);
|
using Graphics graphics = Graphics.FromImage(bitmap);
|
||||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||||
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
graphics.Clear(Color.Transparent);
|
graphics.Clear(Color.Transparent);
|
||||||
|
|
||||||
using SolidBrush background = new(Color.FromArgb(0, 83, 155));
|
using Stream sourceStream = typeof(ProviderTileIcon).Assembly.GetManifestResourceStream(MascotResourceName)
|
||||||
graphics.FillEllipse(background, 1, 1, Size - 2, Size - 2);
|
?? throw new InvalidOperationException($"The branded Credential Provider logo '{MascotResourceName}' is unavailable.");
|
||||||
|
using Bitmap mascot = new(sourceStream);
|
||||||
using Pen key = new(Color.White, 5.5f)
|
using GraphicsPath circularMask = new();
|
||||||
{
|
circularMask.AddEllipse(0, 0, Size, Size);
|
||||||
StartCap = LineCap.Round,
|
graphics.SetClip(circularMask);
|
||||||
EndCap = LineCap.Round,
|
graphics.DrawImage(mascot, new Rectangle(0, 0, Size, Size));
|
||||||
LineJoin = LineJoin.Round
|
|
||||||
};
|
|
||||||
|
|
||||||
graphics.DrawEllipse(key, 14, 14, 25, 25);
|
|
||||||
graphics.DrawLine(key, 35, 35, 57, 57);
|
|
||||||
graphics.DrawLine(key, 47, 47, 55, 39);
|
|
||||||
graphics.DrawLine(key, 53, 53, 61, 45);
|
|
||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
<ProjectReference Include="..\SGU.AuthBroker.Core\SGU.AuthBroker.Core.csproj" />
|
<ProjectReference Include="..\SGU.AuthBroker.Core\SGU.AuthBroker.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="..\..\assets\branding\lasalle-mascot-provider.png"
|
||||||
|
LogicalName="SGU.CredentialProvider.Branding.LaSalleMascot.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||||
<_Parameter1>SGU.CredentialProvider.Tests</_Parameter1>
|
<_Parameter1>SGU.CredentialProvider.Tests</_Parameter1>
|
||||||
|
|||||||
@@ -20,22 +20,27 @@ public sealed class ProviderTileIconTests
|
|||||||
Assert.Equal(0, logo.Bitmap.GetPixel(ProviderTileIcon.Size - 1, 0).A);
|
Assert.Equal(0, logo.Bitmap.GetPixel(ProviderTileIcon.Size - 1, 0).A);
|
||||||
Assert.Equal(0, logo.Bitmap.GetPixel(0, ProviderTileIcon.Size - 1).A);
|
Assert.Equal(0, logo.Bitmap.GetPixel(0, ProviderTileIcon.Size - 1).A);
|
||||||
Assert.Equal(0, logo.Bitmap.GetPixel(ProviderTileIcon.Size - 1, ProviderTileIcon.Size - 1).A);
|
Assert.Equal(0, logo.Bitmap.GetPixel(ProviderTileIcon.Size - 1, ProviderTileIcon.Size - 1).A);
|
||||||
Assert.Equal(
|
int redPixels = 0;
|
||||||
Color.FromArgb(0, 83, 155).ToArgb(),
|
int navyPixels = 0;
|
||||||
logo.Bitmap.GetPixel(6, ProviderTileIcon.Size / 2).ToArgb());
|
|
||||||
int lightPixels = 0;
|
|
||||||
for (int x = 0; x < logo.Bitmap.Width; x++)
|
for (int x = 0; x < logo.Bitmap.Width; x++)
|
||||||
{
|
{
|
||||||
for (int y = 0; y < logo.Bitmap.Height; y++)
|
for (int y = 0; y < logo.Bitmap.Height; y++)
|
||||||
{
|
{
|
||||||
if (logo.Bitmap.GetPixel(x, y).GetBrightness() > 0.7f)
|
Color pixel = logo.Bitmap.GetPixel(x, y);
|
||||||
|
if (pixel.R > 160 && pixel.G < 100 && pixel.B < 100)
|
||||||
{
|
{
|
||||||
lightPixels++;
|
redPixels++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixel.B > pixel.R && pixel.B > pixel.G && pixel.R < 70)
|
||||||
|
{
|
||||||
|
navyPixels++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.InRange(lightPixels, 200, 2_000);
|
Assert.InRange(redPixels, 100, 2_000);
|
||||||
|
Assert.InRange(navyPixels, 100, 4_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user