Bugfix
All checks were successful
release-tag / release-image (push) Successful in 1m30s

This commit is contained in:
2026-08-05 12:01:06 +02:00
parent d191b871de
commit 6ebdcd4fb0
14 changed files with 178 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
779282028f0a75f3906c2c7ac0cb4d914141fc00434abda1f8860db9a1544b10 gpo-agent-windows-amd64.exe
4be4abc096141fa575fab85ec2b5e84b0b5ecf0209bfeaa816bbc943a078a184 gpo-server
614af8ded1c092425fe39d6310518c11e6324d4eaba227ad923cbbc3df8421e0 gpoctl
0865f3bc109bffc535378abdb899dc006ff4b8585538653d6ae9c6e06af413bc gpoctl-windows-amd64.exe
71175431664b4dd8887cb88c51f348994f7ddd7628009a254fe6b47ec6c98fca bin/gpo-server
52ab7f3dfb3bbe6700f7e728662154d58f5a7e2a3059e86c8b2d75018fe4b28a bin/gpoctl
f197120c5fb41dc67501024ead1237b0066f9d3513bfb138270e8258d3450cac bin/gpo-agent-windows-amd64.exe
7994c7baffb5a63b90b5d3816035cfe4d0e35f5795d4fc673dac9837fc65d5dd bin/gpoctl-windows-amd64.exe

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26
ARG ALPINE_VERSION=3.24
ARG GO_VERSION=1.23
ARG ALPINE_VERSION=3.21
FROM golang:${GO_VERSION}-alpine AS build
WORKDIR /src

View File

@@ -103,13 +103,13 @@ Linux/macOS:
```bash
make test
make build VERSION=0.2.1
make build VERSION=0.2.2
```
Windows PowerShell:
```powershell
.\scripts\Build.ps1 -Version 0.2.1
.\scripts\Build.ps1 -Version 0.2.2
```
Erzeugte Dateien:
@@ -280,6 +280,19 @@ Optionaler Sonderfall: trotz identischem semantischem Hash eine Version erzwinge
Das Skript verwendet `Backup-GPO`, erstellt ein ZIP mit korrektem Sicherungswurzelverzeichnis und lädt es hoch. Bei unverändertem Richtlinieninhalt wird keine neue Version erzeugt.
`GpoName` ist der exakte Anzeigename im Active Directory. `PolicyName` ist dagegen der technische Name im Repository und muss `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` entsprechen. Seit Version 0.2.2 ist `PolicyName` optional: Fehlt er oder enthält er Leerzeichen, Umlaute oder andere unzulässige Zeichen, erzeugt das Skript automatisch einen sicheren Namen und zeigt ihn vor dem Upload an. Mit `-StrictPolicyName` kann stattdessen das frühere strikte Verhalten erzwungen werden.
Beispiel:
```powershell
# Wird automatisch als "Server-Windows-Firewall" veröffentlicht.
.\scripts\Export-And-Publish.ps1 `
-GpoName 'Server - Windows Firewall' `
-ServerUrl 'https://gpo.example.org:8443' `
-AdminToken $env:GPO_ADMIN_TOKEN `
-GpoCtl '.\bin\gpoctl-windows-amd64.exe'
```
## Profil erstellen
Das folgende Profil verwendet jeweils die aktuelle Version. Die Reihenfolge ist relevant:

View File

@@ -1,3 +1,15 @@
# Release 0.2.2
Export and naming robustness update.
Included:
- `Export-And-Publish.ps1` now treats `GpoName` and repository `PolicyName` as separate concepts.
- `PolicyName` is optional and is automatically normalized when it contains spaces, umlauts, path separators or other unsupported characters.
- The resolved repository name is printed before the AD backup and upload start.
- `-StrictPolicyName` retains fail-fast validation when automatic normalization is not desired.
- `gpoctl` validates policy and profile names locally and reports the offending value before sending an HTTP request.
# Release 0.2.1
Container deployment update.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -13,6 +13,7 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
@@ -21,6 +22,8 @@ import (
var version = "dev"
var validRepositoryName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
type clientConfig struct {
server string
token string
@@ -81,6 +84,9 @@ func upload(args []string) error {
if *policy == "" || *file == "" {
return errors.New("-policy and -file are required")
}
if err := validateRepositoryName("-policy", *policy); err != nil {
return err
}
f, err := os.Open(*file)
if err != nil {
return err
@@ -135,12 +141,18 @@ func profileSet(args []string) error {
if *name == "" || len(policies) == 0 {
return errors.New("-name and at least one -policy are required")
}
if err := validateRepositoryName("-name", *name); err != nil {
return err
}
refs := make([]model.ProfilePolicy, 0, len(policies))
for _, value := range policies {
policy, ver, ok := strings.Cut(value, "@")
if !ok || policy == "" || ver == "" {
return fmt.Errorf("invalid policy reference %q; expected name@latest or name@version", value)
}
if err := validateRepositoryName("policy reference", policy); err != nil {
return err
}
refs = append(refs, model.ProfilePolicy{Policy: policy, Version: ver})
}
body, _ := json.Marshal(map[string]any{"policies": refs})
@@ -172,6 +184,13 @@ func getList(args []string, path string) error {
return doAndPrint(cfg, req)
}
func validateRepositoryName(label, value string) error {
if !validRepositoryName.MatchString(value) {
return fmt.Errorf("invalid %s %q: must match %s (maximum 64 characters; no spaces)", label, value, validRepositoryName.String())
}
return nil
}
func validateClient(server, token string, insecure bool) (clientConfig, error) {
if server == "" || token == "" {
return clientConfig{}, errors.New("server and token are required")

32
cmd/gpoctl/main_test.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import "testing"
func TestValidateRepositoryName(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value string
ok bool
}{
{name: "simple", value: "windows-firewall", ok: true},
{name: "dots and underscores", value: "MSFT.Server_2022", ok: true},
{name: "space", value: "Server Windows Firewall", ok: false},
{name: "umlaut", value: "server-härtung", ok: false},
{name: "leading hyphen", value: "-server", ok: false},
{name: "too long", value: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ok: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateRepositoryName("test", tt.value)
if tt.ok && err != nil {
t.Fatalf("expected valid name, got %v", err)
}
if !tt.ok && err == nil {
t.Fatalf("expected invalid name")
}
})
}
}

View File

@@ -2,12 +2,12 @@ name: gpo-distributor
services:
gpo-server:
image: "${GPO_SERVER_IMAGE:-gpo-distributor-server:0.2.1}"
image: "${GPO_SERVER_IMAGE:-gpo-distributor-server:0.2.2}"
build:
context: .
dockerfile: Dockerfile
args:
VERSION: "${GPO_SERVER_VERSION:-0.2.1}"
VERSION: "${GPO_SERVER_VERSION:-0.2.2}"
restart: unless-stopped
init: true
stop_grace_period: 25s

View File

@@ -3,12 +3,12 @@ name: gpo-distributor
services:
gpo-server:
image: "${GPO_SERVER_IMAGE:-gpo-distributor-server:0.2.1}"
image: "${GPO_SERVER_IMAGE:-gpo-distributor-server:0.2.2}"
build:
context: ..
dockerfile: Dockerfile
args:
VERSION: "${GPO_SERVER_VERSION:-0.2.1}"
VERSION: "${GPO_SERVER_VERSION:-0.2.2}"
restart: unless-stopped
init: true
stop_grace_period: 25s

2
go.mod
View File

@@ -1,3 +1,3 @@
module gpo-distributor
go 1.26
go 1.23

View File

@@ -2,7 +2,7 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $GpoName,
[Parameter(Mandatory)] [string] $PolicyName,
[string] $PolicyName = '',
[Parameter(Mandatory)] [string] $ServerUrl,
[Parameter(Mandatory)] [string] $AdminToken,
[Parameter(Mandatory)] [string] $GpoCtl,
@@ -10,11 +10,82 @@ param(
[string] $Domain,
[string] $DomainController,
[switch] $InsecureSkipVerify,
[switch] $ForceVersion
[switch] $ForceVersion,
[switch] $StrictPolicyName
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $GpoCtl -PathType Leaf)) { throw "gpoctl not found: $GpoCtl" }
$policyNamePattern = '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'
function ConvertTo-RepositoryPolicyName {
[CmdletBinding()]
param([Parameter(Mandatory)] [string] $Value)
$valueToConvert = $Value.Trim()
if ([string]::IsNullOrWhiteSpace($valueToConvert)) {
throw 'PolicyName must not be empty.'
}
# Remove diacritics where possible and replace every sequence of characters
# that is unsafe for a repository identifier with a single hyphen.
$decomposed = $valueToConvert.Normalize([Text.NormalizationForm]::FormD)
$builder = [Text.StringBuilder]::new()
$separatorPending = $false
foreach ($character in $decomposed.ToCharArray()) {
$category = [Globalization.CharUnicodeInfo]::GetUnicodeCategory($character)
if ($category -eq [Globalization.UnicodeCategory]::NonSpacingMark) {
continue
}
$text = [string] $character
if ($text -match '^[A-Za-z0-9._-]$') {
[void] $builder.Append($character)
$separatorPending = $false
}
elseif (-not $separatorPending -and $builder.Length -gt 0) {
[void] $builder.Append('-')
$separatorPending = $true
}
}
$converted = $builder.ToString()
$converted = $converted -replace '-{2,}', '-'
$converted = $converted.Trim([char[]]'.-_')
if ([string]::IsNullOrWhiteSpace($converted)) {
throw "PolicyName '$Value' cannot be converted to a valid repository name."
}
if ($converted[0] -notmatch '[A-Za-z0-9]') {
$converted = 'gpo-' + $converted
}
if ($converted.Length -gt 64) {
$converted = $converted.Substring(0, 64).TrimEnd([char[]]'.-_')
}
if ($converted -notmatch $policyNamePattern) {
throw "PolicyName '$Value' cannot be converted to a name matching $policyNamePattern."
}
return $converted
}
if (-not (Test-Path -LiteralPath $GpoCtl -PathType Leaf)) {
throw "gpoctl not found: $GpoCtl"
}
$requestedPolicyName = if ([string]::IsNullOrWhiteSpace($PolicyName)) { $GpoName } else { $PolicyName.Trim() }
if ($requestedPolicyName -match $policyNamePattern) {
$resolvedPolicyName = $requestedPolicyName
}
elseif ($StrictPolicyName) {
throw "PolicyName '$requestedPolicyName' is invalid. It must match $policyNamePattern (maximum 64 characters; no spaces or umlauts)."
}
else {
$resolvedPolicyName = ConvertTo-RepositoryPolicyName -Value $requestedPolicyName
Write-Warning "PolicyName '$requestedPolicyName' was normalized to '$resolvedPolicyName'."
}
Write-Host "Publishing AD GPO '$GpoName' as repository policy '$resolvedPolicyName'."
$work = Join-Path ([IO.Path]::GetTempPath()) ("gpo-publish-" + [guid]::NewGuid().ToString('N'))
$backup = Join-Path $work 'backup'
$zip = Join-Path $work 'gpo-backup.zip'
@@ -36,12 +107,21 @@ try {
$false
)
$args = @('upload', '-server', $ServerUrl, '-token', $AdminToken, '-policy', $PolicyName, '-file', $zip)
if ($Note) { $args += @('-note', $Note) }
if ($InsecureSkipVerify) { $args += '-insecure-skip-verify' }
if ($ForceVersion) { $args += '-force' }
& $GpoCtl @args
if ($LASTEXITCODE -ne 0) { throw "gpoctl exited with code $LASTEXITCODE" }
$arguments = @(
'upload',
'-server', $ServerUrl,
'-token', $AdminToken,
'-policy', $resolvedPolicyName,
'-file', $zip
)
if ($Note) { $arguments += @('-note', $Note) }
if ($InsecureSkipVerify) { $arguments += '-insecure-skip-verify' }
if ($ForceVersion) { $arguments += '-force' }
& $GpoCtl @arguments
if ($LASTEXITCODE -ne 0) {
throw "gpoctl exited with code $LASTEXITCODE"
}
}
finally {
Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue