Add Azure user roaming and reproducible Laboratorio wallpaper policy

This commit is contained in:
2026-09-17 16:40:44 -06:00
parent 7460f9316d
commit 7de6e2d867
18 changed files with 1258 additions and 6 deletions
+37 -1
View File
@@ -18,6 +18,17 @@ param(
[string]$VpnClientAddressPoolPrefix = '172.30.0.0/24',
[string[]]$PublicEnrollmentSourceAddressPrefixes = @(),
[string]$AdministratorSourceAddressPrefix = '',
[bool]$DeployUserRoaming = $true,
[ValidatePattern('^$|^[a-z0-9]{3,24}$')]
[string]$UserRoamingStorageAccountName = '',
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
[string]$FsLogixProfilesShareName = 'profiles',
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
[string]$RedirectedFoldersShareName = 'redirected',
[ValidateRange(100, 102400)]
[int]$FsLogixProfilesQuotaGiB = 1024,
[ValidateRange(100, 102400)]
[int]$RedirectedFoldersQuotaGiB = 1024,
[string]$TemplateFile = (Join-Path $PSScriptRoot '..\infra\azure\main.bicep')
)
@@ -33,6 +44,17 @@ if (-not (Test-Path -LiteralPath $TemplateFile -PathType Leaf)) {
if (-not $AdministratorPassword) {
$AdministratorPassword = Read-Host 'Password for the local Azure VM administrator' -AsSecureString
}
if ($DeployUserRoaming -and -not $DeployVpnGateway) {
throw 'Azure user roaming requires the P2S gateway deployed by this template so clients can reach the private Azure Files endpoint. Use -DeployUserRoaming $false with direct public enrollment.'
}
foreach ($shareName in @($FsLogixProfilesShareName, $RedirectedFoldersShareName)) {
if ($shareName.Contains('--')) {
throw "Azure Files share names cannot contain consecutive hyphens: $shareName"
}
}
if ($FsLogixProfilesShareName -eq $RedirectedFoldersShareName) {
throw 'FsLogixProfilesShareName and RedirectedFoldersShareName must be different.'
}
$rootCertificateData = ''
if ($DeployVpnGateway) {
@@ -58,7 +80,10 @@ if ($LASTEXITCODE -ne 0) {
throw "Could not select Azure subscription $SubscriptionId."
}
$deploymentDescription = if ($DeployVpnGateway) {
$deploymentDescription = if ($DeployVpnGateway -and $DeployUserRoaming) {
'Create Azure VNet, Windows Server 2025 VM, public IP, P2S VPN Gateway, and private user-roaming storage'
}
elseif ($DeployVpnGateway) {
'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway'
}
else {
@@ -105,6 +130,12 @@ if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", $deploymentDescri
p2sRootCertificateData = @{ value = $rootCertificateData }
publicEnrollmentSourceAddressPrefixes = @{ value = @($PublicEnrollmentSourceAddressPrefixes) }
administratorSourceAddressPrefix = @{ value = $AdministratorSourceAddressPrefix }
deployUserRoaming = @{ value = $DeployUserRoaming }
userRoamingStorageAccountName = @{ value = $UserRoamingStorageAccountName }
fsLogixProfilesShareName = @{ value = $FsLogixProfilesShareName }
redirectedFoldersShareName = @{ value = $RedirectedFoldersShareName }
fsLogixProfilesQuotaGiB = @{ value = $FsLogixProfilesQuotaGiB }
redirectedFoldersQuotaGiB = @{ value = $RedirectedFoldersQuotaGiB }
}
}
[IO.File]::WriteAllText(
@@ -152,5 +183,10 @@ if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", $deploymentDescri
DeployVpnGateway = $DeployVpnGateway
PublicEnrollmentSourceAddressPrefixes = @($PublicEnrollmentSourceAddressPrefixes)
ServerBootstrapArguments = $values.serverBootstrapArguments
UserRoamingEnabled = [bool]$values.userRoamingEnabled
UserRoamingStorageAccountName = $values.userRoamingStorageAccountName
FsLogixProfilesSharePath = $values.fsLogixProfilesSharePath
RedirectedFoldersSharePath = $values.redirectedFoldersSharePath
UserRoamingSetupArguments = @($values.userRoamingSetupArguments)
}
}
+430
View File
@@ -0,0 +1,430 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][guid]$SubscriptionId,
[Parameter(Mandatory)][string]$ResourceGroupName,
[Parameter(Mandatory)]
[ValidatePattern('^[a-z0-9]{3,24}$')]
[string]$StorageAccountName,
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
[string]$FsLogixProfilesShareName = 'profiles',
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
[string]$RedirectedFoldersShareName = 'redirected',
[string]$DomainController = $env:COMPUTERNAME,
[string]$AzureFilesOuName = 'AzureFilesConfig',
[string]$StudentOuName = 'Alumnos',
[string]$ProfessorOuName = 'Docentes',
[string]$AdministrativeOuName = 'Administrativos',
[string]$LaboratoryOuName = 'Laboratorio',
[string]$StudentGroupName = 'SGU-Alumnos',
[string]$ProfessorGroupName = 'SGU-Docentes',
[string]$AdministrativeGroupName = 'SGU-Administrativos',
[string]$StudentGpoName = 'SGU - AL redirected folders',
[string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles',
[ValidateRange(1024, 1048576)]
[int]$FsLogixProfileSizeMiB = 30000,
[string]$AzFilesHybridModulePath,
[switch]$UseDeviceAuthentication,
[switch]$DeleteExistingStaffLocalProfiles
)
$ErrorActionPreference = 'Stop'
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated Windows PowerShell 5.1 session on the SGU domain controller.'
}
}
function Import-SguAzFilesHybrid {
param([string]$ModulePath)
if ($ModulePath) {
if (-not (Test-Path -LiteralPath $ModulePath)) {
throw "AzFilesHybridModulePath does not exist: $ModulePath"
}
$resolvedModule = if (Test-Path -LiteralPath $ModulePath -PathType Container) {
Get-ChildItem -LiteralPath $ModulePath -Recurse -File |
Where-Object Name -in @('AzFilesHybrid.psd1', 'AzFilesHybrid.psm1') |
Sort-Object @{ Expression = { $_.Extension -eq '.psd1' }; Descending = $true }, FullName |
Select-Object -First 1
}
else {
Get-Item -LiteralPath $ModulePath
}
if (-not $resolvedModule) {
throw "AzFilesHybrid.psd1 or AzFilesHybrid.psm1 was not found beneath $ModulePath."
}
Import-Module -Name $resolvedModule.FullName -Force -ErrorAction Stop
}
else {
Import-Module -Name AzFilesHybrid -Force -ErrorAction Stop
}
$joinCommand = Get-Command Join-AzStorageAccount -ErrorAction SilentlyContinue
if (-not $joinCommand) {
$joinCommand = Get-Command Join-AzStorageAccountForAuth -ErrorAction SilentlyContinue
}
if (-not $joinCommand) {
throw 'AzFilesHybrid did not expose Join-AzStorageAccount. Install the current Microsoft AzFilesHybrid module and retry.'
}
return $joinCommand
}
function Get-SguStorageSamAccountName {
param([Parameter(Mandatory)][string]$StorageName)
if ($StorageName.Length -le 20) {
return $StorageName
}
$sha256 = [Security.Cryptography.SHA256]::Create()
try {
$hash = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($StorageName))
$suffix = ([BitConverter]::ToString($hash) -replace '-', '').Substring(0, 15).ToLowerInvariant()
return "sgufs$suffix"
}
finally {
$sha256.Dispose()
}
}
function Ensure-SguOrganizationalUnit {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Server
)
$escapedName = $Name.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29')
$ou = Get-ADOrganizationalUnit -LDAPFilter "(ou=$escapedName)" -SearchBase $Path `
-SearchScope OneLevel -Server $Server -ErrorAction Stop | Select-Object -First 1
if (-not $ou -and $PSCmdlet.ShouldProcess("OU=$Name,$Path", 'Create Azure Files identity OU')) {
New-ADOrganizationalUnit -Name $Name -Path $Path -ProtectedFromAccidentalDeletion $true `
-Server $Server | Out-Null
$ou = Get-ADOrganizationalUnit -Identity "OU=$Name,$Path" -Server $Server
}
if (-not $ou) {
throw "The organizational unit OU=$Name,$Path does not exist."
}
return $ou
}
function Ensure-SguGpoLink {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$TargetOuDn,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server
)
$gpo = Get-GPO -Name $Name -Domain $DomainName -Server $Server -ErrorAction SilentlyContinue
if (-not $gpo -and $PSCmdlet.ShouldProcess($Name, 'Create user-roaming GPO')) {
$gpo = New-GPO -Name $Name -Domain $DomainName -Server $Server
}
if (-not $gpo) {
throw "The GPO '$Name' does not exist and was not created."
}
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $DomainName -Server $Server).GpoLinks |
Where-Object DisplayName -eq $Name | Select-Object -First 1
$linkEnabled = $link -and ($link.Enabled -eq $true -or [string]$link.Enabled -eq 'Yes')
if (-not $link -and $PSCmdlet.ShouldProcess($TargetOuDn, "Link and enable '$Name'")) {
New-GPLink -Name $Name -Target $TargetOuDn -Domain $DomainName -Server $Server `
-LinkEnabled Yes | Out-Null
}
elseif ($link -and -not $linkEnabled -and
$PSCmdlet.ShouldProcess($TargetOuDn, "Enable the '$Name' link")) {
Set-GPLink -Name $Name -Target $TargetOuDn -Domain $DomainName -Server $Server `
-LinkEnabled Yes | Out-Null
}
return $gpo
}
function Set-SguGpoRegistryValue {
param(
[Parameter(Mandatory)][string]$GpoName,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Key,
[Parameter(Mandatory)][string]$ValueName,
[Parameter(Mandatory)][ValidateSet('DWord', 'String', 'ExpandString')][string]$Type,
[Parameter(Mandatory)]$Value
)
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set $Type policy value")) {
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
}
}
function Get-SguUnusedDriveName {
$used = @(Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Name)
foreach ($name in @('Z', 'Y', 'X', 'W', 'V')) {
if ($used -notcontains $name) {
return $name
}
}
throw 'No temporary drive letter is available for configuring Azure Files ACLs.'
}
function Set-SguAzureFileRootAcl {
param(
[Parameter(Mandatory)][string]$UncPath,
[Parameter(Mandatory)][PSCredential]$Credential,
[Parameter(Mandatory)][Security.Principal.SecurityIdentifier]$DomainAdminsSid,
[Parameter(Mandatory)][Security.Principal.SecurityIdentifier[]]$ContributorSids,
[Security.AccessControl.FileSystemRights]$ContributorRights =
[Security.AccessControl.FileSystemRights]::Modify
)
$driveName = Get-SguUnusedDriveName
try {
New-PSDrive -Name $driveName -PSProvider FileSystem -Root $UncPath `
-Credential $Credential -Scope Script -ErrorAction Stop | Out-Null
$rootPath = "${driveName}:\"
$acl = [Security.AccessControl.DirectorySecurity]::new()
$acl.SetAccessRuleProtection($true, $false)
$acl.SetOwner($DomainAdminsSid)
$allow = [Security.AccessControl.AccessControlType]::Allow
$containerAndObject = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
$noneInheritance = [Security.AccessControl.InheritanceFlags]::None
$nonePropagation = [Security.AccessControl.PropagationFlags]::None
$inheritOnly = [Security.AccessControl.PropagationFlags]::InheritOnly
$systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18')
$creatorOwnerSid = [Security.Principal.SecurityIdentifier]::new('S-1-3-0')
foreach ($administratorSid in @($systemSid, $DomainAdminsSid)) {
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
$administratorSid,
[Security.AccessControl.FileSystemRights]::FullControl,
$containerAndObject,
$nonePropagation,
$allow))
}
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
$creatorOwnerSid,
[Security.AccessControl.FileSystemRights]::Modify,
$containerAndObject,
$inheritOnly,
$allow))
foreach ($contributorSid in $ContributorSids) {
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
$contributorSid,
$ContributorRights,
$noneInheritance,
$nonePropagation,
$allow))
}
Set-Acl -LiteralPath $rootPath -AclObject $acl -ErrorAction Stop
}
finally {
Remove-PSDrive -Name $driveName -Scope Script -Force -ErrorAction SilentlyContinue
}
}
Assert-Administrator
if ($FsLogixProfilesShareName.Contains('--') -or $RedirectedFoldersShareName.Contains('--')) {
throw 'Azure Files share names cannot contain consecutive hyphens.'
}
if ($FsLogixProfilesShareName -eq $RedirectedFoldersShareName) {
throw 'The profile-container and redirected-folder shares must have different names.'
}
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
foreach ($azureModule in @('Az.Accounts', 'Az.Storage')) {
try {
Import-Module $azureModule -ErrorAction Stop
}
catch {
throw "The current $azureModule module is required on the domain controller. Install Azure PowerShell and retry. $($_.Exception.Message)"
}
}
$joinStorageCommand = Import-SguAzFilesHybrid -ModulePath $AzFilesHybridModulePath
$domain = Get-ADDomain -Server $DomainController
$baseDn = $domain.DistinguishedName
$domainName = $domain.DNSRoot
$usersOuDn = "OU=Usuarios-SGU,$baseDn"
$studentOuDn = "OU=$StudentOuName,$usersOuDn"
$professorOuDn = "OU=$ProfessorOuName,$usersOuDn"
$administrativeOuDn = "OU=$AdministrativeOuName,$usersOuDn"
$laboratoryOuDn = "OU=$LaboratoryOuName,$baseDn"
foreach ($requiredOu in @($studentOuDn, $professorOuDn, $administrativeOuDn, $laboratoryOuDn)) {
Get-ADOrganizationalUnit -Identity $requiredOu -Server $DomainController -ErrorAction Stop | Out-Null
}
$studentGroup = Get-ADGroup -Identity "CN=$StudentGroupName,$studentOuDn" -Server $DomainController
$professorGroup = Get-ADGroup -Identity "CN=$ProfessorGroupName,$professorOuDn" -Server $DomainController
$administrativeGroup = Get-ADGroup -Identity "CN=$AdministrativeGroupName,$administrativeOuDn" `
-Server $DomainController
$domainAdminsSid = [Security.Principal.SecurityIdentifier]::new("$($domain.DomainSID.Value)-512")
$azureFilesOu = Ensure-SguOrganizationalUnit -Name $AzureFilesOuName -Path $baseDn -Server $DomainController
$azureContext = Get-AzContext -ErrorAction SilentlyContinue
if (-not $azureContext -or $azureContext.Subscription.Id -ne $SubscriptionId.Guid) {
$connectParameters = @{}
if ($UseDeviceAuthentication) {
$connectParameters.UseDeviceAuthentication = $true
}
Connect-AzAccount @connectParameters | Out-Null
}
Set-AzContext -SubscriptionId $SubscriptionId.Guid | Out-Null
$storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName -ErrorAction Stop
$fileEndpointHost = ([uri]$storageAccount.PrimaryEndpoints.File).Host
if (-not $fileEndpointHost) {
throw "Azure did not return a file endpoint for $StorageAccountName."
}
$directoryService = [string]$storageAccount.AzureFilesIdentityBasedAuth.DirectoryServiceOptions
if ($directoryService -and $directoryService -ne 'None' -and $directoryService -ne 'AD') {
throw "Storage account $StorageAccountName already uses the incompatible Azure Files identity source '$directoryService'."
}
if ($directoryService -ne 'AD') {
if ($PSCmdlet.ShouldProcess($StorageAccountName, "Join Azure Files to $domainName with AES-256 Kerberos")) {
$requestedSamAccountName = Get-SguStorageSamAccountName -StorageName $StorageAccountName
$joinParameters = @{
ResourceGroupName = $ResourceGroupName
StorageAccountName = $StorageAccountName
SamAccountName = $requestedSamAccountName
DomainAccountType = 'ComputerAccount'
OrganizationalUnitDistinguishedName = $azureFilesOu.DistinguishedName
}
& $joinStorageCommand @joinParameters
$storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName -ErrorAction Stop
}
}
$directoryService = [string]$storageAccount.AzureFilesIdentityBasedAuth.DirectoryServiceOptions
if ($directoryService -ne 'AD') {
throw "Azure Files identity authentication is '$directoryService', not AD. The domain join did not complete."
}
$activeDirectoryProperties = $storageAccount.AzureFilesIdentityBasedAuth.ActiveDirectoryProperties
if ([string]$activeDirectoryProperties.DomainName -ne $domainName) {
throw "Storage account $StorageAccountName is joined to $($activeDirectoryProperties.DomainName), not $domainName."
}
$storageSamAccountName = [string]$activeDirectoryProperties.SamAccountName
if (-not $storageSamAccountName) {
$storageSamAccountName = $StorageAccountName
}
$storageComputer = Get-ADComputer -Identity "${storageSamAccountName}$" `
-Server $DomainController -ErrorAction Stop
if ($PSCmdlet.ShouldProcess($storageComputer.DistinguishedName, 'Require AES-256 Kerberos and prevent an unattended storage identity password expiry')) {
Set-ADComputer -Identity $storageComputer -Server $DomainController `
-KerberosEncryptionType AES256 -PasswordNeverExpires $true
}
if ($PSCmdlet.ShouldProcess($StorageAccountName, 'Grant authenticated AD identities the Azure Files SMB contributor default share permission')) {
$storageAccount = Set-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName `
-DefaultSharePermission StorageFileDataSmbShareContributor
}
$privateAddresses = @(Resolve-DnsName -Name $fileEndpointHost -Type A -ErrorAction Stop |
Where-Object IPAddress | Select-Object -ExpandProperty IPAddress)
if ($privateAddresses.Count -eq 0 -or @($privateAddresses | Where-Object {
$_ -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)'
}).Count -eq 0) {
throw "$fileEndpointHost did not resolve to a private endpoint. Verify the privatelink.file.core.windows.net VNet link and the DC Azure DNS forwarder."
}
if (-not (Test-NetConnection -ComputerName $fileEndpointHost -Port 445 -InformationLevel Quiet)) {
throw "The domain controller cannot reach $fileEndpointHost on TCP 445 through the private endpoint."
}
$storageKey = @(Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName -ErrorAction Stop | Where-Object KeyName -eq 'key1' |
Select-Object -First 1).Value
if (-not $storageKey) {
throw "Azure did not return key1 for $StorageAccountName; it is required only to set the initial root ACLs."
}
$storageCredential = [PSCredential]::new(
"Azure\$StorageAccountName",
(ConvertTo-SecureString -String $storageKey -AsPlainText -Force))
$profilesSharePath = "\\$fileEndpointHost\$FsLogixProfilesShareName"
$redirectedFoldersSharePath = "\\$fileEndpointHost\$RedirectedFoldersShareName"
try {
if ($PSCmdlet.ShouldProcess($profilesSharePath, 'Apply isolated FSLogix root ACLs')) {
Set-SguAzureFileRootAcl -UncPath $profilesSharePath -Credential $storageCredential `
-DomainAdminsSid $domainAdminsSid `
-ContributorSids @($professorGroup.SID, $administrativeGroup.SID)
}
if ($PSCmdlet.ShouldProcess($redirectedFoldersSharePath, 'Apply isolated student-folder root ACLs')) {
$studentRootRights = [Security.AccessControl.FileSystemRights]::CreateDirectories -bor
[Security.AccessControl.FileSystemRights]::ListDirectory -bor
[Security.AccessControl.FileSystemRights]::ReadAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadExtendedAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadPermissions -bor
[Security.AccessControl.FileSystemRights]::Traverse -bor
[Security.AccessControl.FileSystemRights]::Synchronize
Set-SguAzureFileRootAcl -UncPath $redirectedFoldersSharePath -Credential $storageCredential `
-DomainAdminsSid $domainAdminsSid -ContributorSids @($studentGroup.SID) `
-ContributorRights $studentRootRights
}
}
finally {
$storageKey = $null
$storageCredential = $null
}
$studentGpo = Ensure-SguGpoLink -Name $StudentGpoName -TargetOuDn $studentOuDn `
-DomainName $domainName -Server $DomainController
$userShellFoldersKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
Set-SguGpoRegistryValue -GpoName $studentGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $userShellFoldersKey -ValueName 'Desktop' `
-Type ExpandString -Value "$redirectedFoldersSharePath\%USERNAME%\Desktop"
Set-SguGpoRegistryValue -GpoName $studentGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $userShellFoldersKey -ValueName 'Personal' `
-Type ExpandString -Value "$redirectedFoldersSharePath\%USERNAME%\Documents"
Set-SguGpoRegistryValue -GpoName $studentGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key 'HKCU\Software\Policies\Microsoft\Windows\NetCache' `
-ValueName 'DisableFRAdminPin' -Type DWord -Value 1
$staffGpo = Ensure-SguGpoLink -Name $StaffGpoName -TargetOuDn $laboratoryOuDn `
-DomainName $domainName -Server $DomainController
$fsLogixRoot = 'HKLM\SOFTWARE\FSLogix\Profiles'
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $fsLogixRoot -ValueName 'Enabled' -Type DWord -Value 0
$fsLogixValues = [ordered]@{
Enabled = @{ Type = 'DWord'; Value = 1 }
DeleteLocalProfileWhenVHDShouldApply = @{
Type = 'DWord'
Value = if ($DeleteExistingStaffLocalProfiles) { 1 } else { 0 }
}
FlipFlopProfileDirectoryName = @{ Type = 'DWord'; Value = 1 }
IsDynamic = @{ Type = 'DWord'; Value = 1 }
LockedRetryCount = @{ Type = 'DWord'; Value = 3 }
LockedRetryInterval = @{ Type = 'DWord'; Value = 15 }
ProfileType = @{ Type = 'DWord'; Value = 0 }
ReAttachIntervalSeconds = @{ Type = 'DWord'; Value = 15 }
ReAttachRetryCount = @{ Type = 'DWord'; Value = 3 }
SizeInMBs = @{ Type = 'DWord'; Value = $FsLogixProfileSizeMiB }
VHDLocations = @{ Type = 'String'; Value = $profilesSharePath }
VolumeType = @{ Type = 'String'; Value = 'VHDX' }
}
foreach ($staffGroup in @($professorGroup, $administrativeGroup)) {
$objectSpecificKey = "$fsLogixRoot\ObjectSpecific\$($staffGroup.SID.Value)"
foreach ($setting in $fsLogixValues.GetEnumerator()) {
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $objectSpecificKey -ValueName $setting.Key `
-Type $setting.Value.Type -Value $setting.Value.Value
}
}
[pscustomobject]@{
StorageAccountName = $StorageAccountName
FileEndpoint = $fileEndpointHost
PrivateEndpointAddresses = $privateAddresses
DirectoryService = $directoryService
KerberosEncryption = 'AES256'
StorageIdentity = $storageComputer.DistinguishedName
StorageIdentityPasswordNeverExpires = $true
ProfilesSharePath = $profilesSharePath
RedirectedFoldersSharePath = $redirectedFoldersSharePath
StudentPolicy = $studentGpo.DisplayName
StaffPolicy = $staffGpo.DisplayName
StudentBehavior = 'Local non-authoritative profile; Documents and Desktop redirected without Offline Files pinning.'
StaffBehavior = 'FSLogix VHDX profile container for SGU-Docentes and SGU-Administrativos only.'
ExistingStaffLocalProfilesDeleted = [bool]$DeleteExistingStaffLocalProfiles
}
@@ -520,6 +520,7 @@ foreach ($requiredPath in @(
(Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1'),
(Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1'),
(Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1'),
(Join-Path $scriptsRoot 'Set-SguLaboratorioWallpaperPolicy.ps1'),
(Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1'),
(Join-Path $scriptsRoot 'Install-SguDomainMonitoring.ps1'),
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
@@ -829,6 +830,8 @@ $userPolicyParameters = @{
ClearManagedWallpaper = $true
}
& (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1') @userPolicyParameters | Out-Null
& (Join-Path $scriptsRoot 'Set-SguLaboratorioWallpaperPolicy.ps1') `
-TargetOuDn $laboratoryOuDn -DomainController $env:COMPUTERNAME | Out-Null
$rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') `
-ServerAddress $rustDeskDnsName `
+67
View File
@@ -0,0 +1,67 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$InstallerPath
)
$ErrorActionPreference = 'Stop'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run the FSLogix installer from an elevated Windows PowerShell session.'
}
$appsRoot = Join-Path $env:ProgramFiles 'FSLogix\Apps'
$frxPath = Join-Path $appsRoot 'frx.exe'
$service = Get-Service -Name frxsvc -ErrorAction SilentlyContinue
if ($service -and (Test-Path -LiteralPath $frxPath -PathType Leaf)) {
$versionOutput = @(& $frxPath version 2>&1) -join [Environment]::NewLine
return [pscustomobject]@{
Installed = $true
Changed = $false
RestartRequired = $false
Service = $service.Status.ToString()
Version = $versionOutput.Trim()
}
}
if (-not (Test-Path -LiteralPath $InstallerPath -PathType Leaf)) {
throw "FSLogixAppsSetup.exe was not found: $InstallerPath"
}
$resolvedInstaller = (Resolve-Path -LiteralPath $InstallerPath).Path
if ([IO.Path]::GetFileName($resolvedInstaller) -ne 'FSLogixAppsSetup.exe') {
throw 'InstallerPath must identify the Microsoft FSLogix core installer named FSLogixAppsSetup.exe.'
}
$signature = Get-AuthenticodeSignature -LiteralPath $resolvedInstaller
if ($signature.Status -ne [Management.Automation.SignatureStatus]::Valid -or
-not $signature.SignerCertificate -or
$signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') {
throw 'FSLogixAppsSetup.exe must have a valid Microsoft Corporation Authenticode signature.'
}
$logRoot = Join-Path $env:ProgramData 'SGU\FSLogix'
$logPath = Join-Path $logRoot 'install.log'
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install Microsoft FSLogix Apps without restarting')) {
return
}
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
$process = Start-Process -FilePath $resolvedInstaller `
-ArgumentList @('/install', '/quiet', '/norestart', '/log', "`"$logPath`"") `
-Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -notin @(0, 1641, 3010)) {
throw "FSLogix installation failed with exit code $($process.ExitCode). Review $logPath."
}
$service = Get-Service -Name frxsvc -ErrorAction SilentlyContinue
if (-not $service -or -not (Test-Path -LiteralPath $frxPath -PathType Leaf)) {
throw "FSLogix installation did not create the frxsvc service and frx.exe. Review $logPath."
}
$versionOutput = @(& $frxPath version 2>&1) -join [Environment]::NewLine
[pscustomobject]@{
Installed = $true
Changed = $true
RestartRequired = $true
Service = $service.Status.ToString()
Version = $versionOutput.Trim()
LogPath = $logPath
}
+16
View File
@@ -21,6 +21,7 @@ param(
[securestring]$VpnClientCertificatePfxPassword,
[string]$VpnClientRootCertificatePath,
[string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'),
[string]$FsLogixInstallerPath,
[switch]$PauseOnError,
[switch]$SkipRestart
)
@@ -551,6 +552,7 @@ $runtimeInstaller = Get-ChildItem (Join-Path $packageRoot 'payload\prerequisites
Select-Object -First 1
foreach ($requiredPath in @(
(Join-Path $scriptsRoot 'Enroll-SguDomainClient.ps1'),
(Join-Path $scriptsRoot 'Install-SguFsLogix.ps1'),
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
(Join-Path $scriptsRoot 'Register-SguClientCertificate.ps1'),
(Join-Path $providerPublishPath 'SGU.CredentialProvider.comhost.dll'))) {
@@ -562,6 +564,18 @@ if (-not $runtimeInstaller) {
throw 'The offline Microsoft .NET 10 x64 runtime installer is missing from the client package.'
}
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enroll with SGU server $DomainControllerIPv4Address")) { return }
$fsLogixResult = $null
if ($FsLogixInstallerPath) {
$fsLogixResult = & (Join-Path $scriptsRoot 'Install-SguFsLogix.ps1') `
-InstallerPath $FsLogixInstallerPath
}
elseif (Get-Service -Name frxsvc -ErrorAction SilentlyContinue) {
$fsLogixResult = [pscustomobject]@{
Installed = $true
Changed = $false
RestartRequired = $false
}
}
if ($ClientIPv4Address) {
if (-not $NetworkInterfaceAlias -or $ConnectivityMode -eq 'AzureP2S') {
throw 'Explicit static IP setup requires -NetworkInterfaceAlias with Direct connectivity. Omit -ClientIPv4Address to preserve the current LAN/VPN configuration.'
@@ -1004,6 +1018,8 @@ if ($SkipRestart) {
ConnectivityMode = $ConnectivityMode
CompatibilityProfile = $CompatibilityProfile
VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null }
FsLogixInstalled = [bool]($fsLogixResult -and $fsLogixResult.Installed)
FsLogixChanged = [bool]($fsLogixResult -and $fsLogixResult.Changed)
RestartRequired = $true
RustDesk = if ($result) { $result.RustDesk } else { $null }
EnrollmentResult = $result
+9
View File
@@ -123,6 +123,7 @@ $clientScripts = @(
'Enable-SguClientMonitoring.ps1',
'Enroll-SguDomainClient.ps1',
'Install-CredentialProvider.ps1',
'Install-SguFsLogix.ps1',
'Install-SguEnrollmentGuard.ps1',
'Install-SguRustDeskClient.ps1',
'Register-SguClientCertificate.ps1',
@@ -195,6 +196,7 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureServerBootstra
-Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd')
$serverScripts = @(
'Deploy-AuthBroker.ps1',
'Enable-SguAzureUserRoaming.ps1',
'Enable-SguServerRemoteManagement.ps1',
'Get-SguUsageReport.ps1',
'Get-SguBrokerLog.ps1',
@@ -210,6 +212,7 @@ $serverScripts = @(
'Register-SguRustDeskDevice.ps1',
'Set-LabBrokerDns.ps1',
'Set-SguDomainComputerPolicies.ps1',
'Set-SguLaboratorioWallpaperPolicy.ps1',
'Set-SguDomainUserPolicies.ps1'
)
foreach ($scriptName in $serverScripts) {
@@ -237,6 +240,10 @@ foreach ($fontName in $welcomeFontNames) {
Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") `
-Destination (Join-Path $serverContentTarget "welcome-wallpaper\fonts\$fontName")
}
foreach ($documentation in @('user-roaming.md', 'laboratorio-wallpaper-policy.md')) {
Copy-RequiredFile -Source (Join-Path $repositoryRoot "docs\$documentation") `
-Destination (Join-Path $serverRoot "docs\$documentation")
}
Write-PackageManifest -PackageRoot $serverRoot -PackageVersion $Version -PackageKind Server
Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip `
-CompressionLevel Optimal
@@ -258,6 +265,8 @@ foreach ($scriptName in @(
}
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\azure-vpn-deployment.md') `
-Destination (Join-Path $azureRoot 'README.md')
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\user-roaming.md') `
-Destination (Join-Path $azureRoot 'user-roaming.md')
Write-PackageManifest -PackageRoot $azureRoot -PackageVersion $Version -PackageKind AzureInfrastructure
Compress-Archive -Path (Join-Path $azureRoot '*') -DestinationPath $azureZip `
-CompressionLevel Optimal
+7
View File
@@ -9,10 +9,14 @@ param(
[string]$Owner = 'alexrg',
[string]$Repository = 'SGU-CredentialProvider',
[string]$TargetCommitish = 'main',
[string]$ReleaseNotesPath,
[switch]$Draft
)
$ErrorActionPreference = 'Stop'
if ($ReleaseNotesPath -and -not (Test-Path -LiteralPath $ReleaseNotesPath -PathType Leaf)) {
throw "Release notes file is missing: $ReleaseNotesPath"
}
$tagName = "v$Version"
$assetPaths = @(
(Join-Path $ReleaseDirectory "sgu-windows-client-bootstrap-$Version.zip"),
@@ -131,6 +135,9 @@ Bootstrap reproducible para el laboratorio SGU.
Las contraseñas se solicitan de forma interactiva y no se escriben en archivos ni en la línea de comandos. Verifique los ZIP con `SHA256SUMS-$Version.txt`.
"@
if ($ReleaseNotesPath) {
$releaseNotes = Get-Content -LiteralPath $ReleaseNotesPath -Raw -Encoding UTF8
}
$releaseBody = [ordered]@{
tag_name = $tagName
target_commitish = $TargetCommitish
@@ -0,0 +1,75 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$TargetOuDn = 'OU=Laboratorio,DC=lci,DC=lasalle,DC=mx',
[string]$GpoName = 'SGU - Laboratorio wallpaper protection',
[string]$DomainController = $env:COMPUTERNAME,
[string]$WallpaperPath = '%LOCALAPPDATA%\SGU\Wallpapers\welcome-%COMPUTERNAME%.jpg'
)
$ErrorActionPreference = 'Stop'
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
$domainName = (Get-ADDomain -Server $DomainController).DNSRoot
Get-ADOrganizationalUnit -Identity $TargetOuDn -Server $DomainController -ErrorAction Stop | Out-Null
if (-not $PSCmdlet.ShouldProcess($TargetOuDn, "Apply '$GpoName' with enforced loopback Merge")) {
return
}
$backupPath = $null
$gpo = Get-GPO -Name $GpoName -Domain $domainName -Server $DomainController -ErrorAction SilentlyContinue
if ($gpo) {
$backupPath = Join-Path $env:ProgramData ('SGU\PolicyBackups\Wallpaper-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $backupPath -Force | Out-Null
Backup-GPO -Guid $gpo.Id -Path $backupPath -Domain $domainName -Server $DomainController | Out-Null
}
else {
$gpo = New-GPO -Name $GpoName -Domain $domainName -Server $DomainController `
-Comment 'Protects the SGU desktop wallpaper on Laboratorio computers and child OUs; loopback Merge preserves existing user policies.'
}
# These are user policies scoped by the computer OU, not by the user OU.
# Match the per-user/per-computer output of Set-SguWelcomeWallpaper.ps1.
$settings = @(
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\System'; Name = 'UserPolicyMode'; Type = 'DWord'; Value = 1 },
@{ Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop'; Name = 'NoChangingWallPaper'; Type = 'DWord'; Value = 1 },
@{ Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'; Name = 'Wallpaper'; Type = 'ExpandString'; Value = $WallpaperPath },
@{ Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'; Name = 'WallpaperStyle'; Type = 'String'; Value = '10' }
)
foreach ($setting in $settings) {
Set-GPRegistryValue -Guid $gpo.Id -Domain $domainName -Server $DomainController `
-Key $setting.Key -ValueName $setting.Name -Type $setting.Type -Value $setting.Value | Out-Null
}
$linkParameters = @{
Guid = $gpo.Id
Target = $TargetOuDn
Domain = $domainName
Server = $DomainController
LinkEnabled = 'Yes'
Enforced = 'Yes'
Order = 1
}
$existingLink = (Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object GpoId -eq $gpo.Id
if ($existingLink) {
Set-GPLink @linkParameters | Out-Null
}
else {
New-GPLink @linkParameters | Out-Null
}
foreach ($setting in $settings) {
$actual = Get-GPRegistryValue -Guid $gpo.Id -Domain $domainName -Server $DomainController `
-Key $setting.Key -ValueName $setting.Name
if ([string]$actual.Value -ne [string]$setting.Value -or [string]$actual.Type -ne $setting.Type) {
throw "Wallpaper policy verification failed for $($setting.Name)."
}
}
[pscustomobject]@{
Name = $gpo.DisplayName
Id = $gpo.Id
TargetOuDn = $TargetOuDn
WallpaperPath = $WallpaperPath
Loopback = 'Merge'
Enforced = $true
BackupPath = $backupPath
}
+2 -1
View File
@@ -4,7 +4,8 @@ set "SGU_BOOTSTRAP_IP=%~1"
set "SGU_VPN_PACKAGE=%~2"
set "SGU_VPN_PFX=%~3"
set "SGU_VPN_ROOT=%~4"
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Invoke-SguClientBootstrap.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-PauseOnError','-ConnectivityMode','AzureP2S'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-DomainControllerIPv4Address',('"' + $env:SGU_BOOTSTRAP_IP + '"')) }; if ($env:SGU_VPN_PACKAGE) { $arguments += @('-VpnProfilePackagePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PACKAGE) + '"')) }; if ($env:SGU_VPN_PFX) { $arguments += @('-VpnClientCertificatePfxPath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PFX) + '"')) }; if ($env:SGU_VPN_ROOT) { $arguments += @('-VpnClientRootCertificatePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_ROOT) + '"')) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode"
set "SGU_FSLOGIX_INSTALLER=%~5"
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Invoke-SguClientBootstrap.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-PauseOnError','-ConnectivityMode','AzureP2S'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-DomainControllerIPv4Address',('"' + $env:SGU_BOOTSTRAP_IP + '"')) }; if ($env:SGU_VPN_PACKAGE) { $arguments += @('-VpnProfilePackagePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PACKAGE) + '"')) }; if ($env:SGU_VPN_PFX) { $arguments += @('-VpnClientCertificatePfxPath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PFX) + '"')) }; if ($env:SGU_VPN_ROOT) { $arguments += @('-VpnClientRootCertificatePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_ROOT) + '"')) }; if ($env:SGU_FSLOGIX_INSTALLER) { $arguments += @('-FsLogixInstallerPath',('"' + [IO.Path]::GetFullPath($env:SGU_FSLOGIX_INSTALLER) + '"')) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode"
set "SGU_EXIT_CODE=%errorlevel%"
if not "%SGU_EXIT_CODE%"=="0" (
echo.