feat: Implement API client and token models with hashing and JWT authentication

- Added ApiClientModel and ApiTokenModel for managing API clients and tokens.
- Introduced ConfigurationDefinitionModel and ConfigurationValueModel for configuration management.
- Created CredentialSecretModel for storing credential secrets.
- Developed DeploymentArtifactModel and DeploymentBatchModel for deployment management.
- Enhanced DeploymentTargetModel and DeploymentTemplateSelectionModel to support template revisions.
- Added TemplateRevisionModel and TemplateVersionModel for versioning templates.
- Implemented ApiClientSecretHasher for secure secret hashing.
- Created ApiTokenService for generating and validating JWT tokens.
- Updated QueueJobService to handle deployment requests with artifacts.
- Configured authentication settings in appsettings.json for JWT and Negotiate authentication.
This commit is contained in:
Torsten Brendgen
2026-07-09 14:44:38 +02:00
parent 1aa2adac08
commit dc93ea0813
72 changed files with 32906 additions and 516 deletions

View File

@@ -0,0 +1,137 @@
param(
[string]$ApiBaseUrl = 'http://localhost:5286/api',
[string]$ClientId = 'ssp-demo-worker',
[string]$ClientSecret = 'DemoOnly-DoNotUseInProduction!',
[int]$ApiTimeoutSec = 30
)
$ErrorActionPreference = 'Stop'
function Invoke-TestApi {
param(
[ValidateSet('GET', 'POST', 'DELETE')]
[string]$Method,
[string]$Path,
[object]$Body,
[string]$BearerToken
)
$uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/'))
$parameters = @{
Method = $Method
Uri = $uri
TimeoutSec = $ApiTimeoutSec
ErrorAction = 'Stop'
}
if ($Body) {
$parameters.Body = ($Body | ConvertTo-Json -Depth 10)
$parameters.ContentType = 'application/json'
}
if ($BearerToken) {
$parameters.Headers = @{
Authorization = "Bearer $BearerToken"
}
}
try {
Invoke-RestMethod @parameters
}
catch {
$responseBody = '<empty response body>'
if ($_.Exception.Response) {
try {
$stream = $_.Exception.Response.GetResponseStream()
if ($stream) {
$reader = [System.IO.StreamReader]::new($stream)
$text = $reader.ReadToEnd()
if (-not [string]::IsNullOrWhiteSpace($text)) {
$responseBody = $text
}
}
}
catch {
$responseBody = '<could not read response body>'
}
}
throw "API request failed. Method=[$Method], Uri=[$uri], Response=[$responseBody]. $($_.Exception.Message)"
}
}
Describe 'On-prem client credentials API authentication' {
It 'issues a bearer token for the seeded demo worker client' {
$token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
clientId = $ClientId
clientSecret = $ClientSecret
scope = 'deployment.read template.read'
}
$token.accessToken | Should Not BeNullOrEmpty
$token.tokenType | Should Be 'Bearer'
$token.expiresIn | Should BeGreaterThan 0
$token.scope | Should Be 'deployment.read template.read'
}
It 'allows bearer-token API calls without Windows authentication' {
$token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
clientId = $ClientId
clientSecret = $ClientSecret
scope = 'deployment.read template.read'
}
$definitions = @(Invoke-TestApi -Method GET -Path 'configuration-definitions?kind=Parameter' -BearerToken $token.accessToken)
@($definitions).Count | Should BeGreaterThan 0
@($definitions | Where-Object { $_.kind -ne 'Parameter' }).Count | Should Be 0
}
It 'creates, lists and revokes managed tokens through the token API' {
$adminToken = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
clientId = $ClientId
clientSecret = $ClientSecret
scope = 'token.manage token.admin deployment.read'
}
$managedToken = Invoke-TestApi -Method POST -Path 'tokens/my' -BearerToken $adminToken.accessToken -Body @{
name = 'Pester managed token'
scope = 'deployment.read'
}
$managedToken.id | Should Not BeNullOrEmpty
$managedToken.accessToken | Should Not BeNullOrEmpty
$managedToken.scope | Should Be 'deployment.read'
$myTokens = @(Invoke-TestApi -Method GET -Path 'tokens/my' -BearerToken $adminToken.accessToken)
@($myTokens | Where-Object { $_.id -eq $managedToken.id }).Count | Should Be 1
$allTokens = @(Invoke-TestApi -Method GET -Path 'tokens/admin' -BearerToken $adminToken.accessToken)
@($allTokens | Where-Object { $_.id -eq $managedToken.id }).Count | Should Be 1
$deletedToken = Invoke-TestApi -Method DELETE -Path ('tokens/my/{0}' -f $managedToken.id) -BearerToken $adminToken.accessToken
$deletedToken.id | Should Be $managedToken.id
$deletedToken.revokedAt | Should Not BeNullOrEmpty
$deletedToken.isActive | Should Be $false
$myTokensAfterDelete = @(Invoke-TestApi -Method GET -Path 'tokens/my' -BearerToken $adminToken.accessToken)
$myTokenAfterDelete = $myTokensAfterDelete | Where-Object { [string]$_.id -eq [string]$managedToken.id } | Select-Object -First 1
if ($myTokenAfterDelete) {
$myTokenAfterDelete.isActive | Should Be $false
}
$adminTokensAfterDelete = @(Invoke-TestApi -Method GET -Path 'tokens/admin' -BearerToken $adminToken.accessToken)
$revokedToken = $adminTokensAfterDelete | Where-Object { [string]$_.id -eq [string]$managedToken.id } | Select-Object -First 1
if ($revokedToken) {
$revokedToken.revokedAt | Should Not BeNullOrEmpty
$revokedToken.isActive | Should Be $false
}
}
}