Add migration to update ApiClients with centralized authorization scopes

This migration updates the ScopesJson for the ApiClient with Id 91000000-0000-0000-0000-000000000001 to include additional authorization scopes for improved API access control. The Down method reverts the changes if necessary.
This commit is contained in:
Torsten Brendgen
2026-07-09 23:49:01 +02:00
parent dc93ea0813
commit dfd9c16f61
27 changed files with 5617 additions and 34 deletions

View File

@@ -0,0 +1,422 @@
param(
[string]$ApiBaseUrl = 'http://localhost:5286/api',
[string]$ClientId = 'ssp-demo-worker',
[string]$ClientSecret = 'DemoOnly-DoNotUseInProduction!',
[int]$ApiTimeoutSec = 30
)
$ErrorActionPreference = 'Stop'
function Invoke-Api {
param(
[ValidateSet('GET', 'POST', 'PUT', 'DELETE')]
[string]$Method,
[string]$Path,
[object]$Body,
[string]$BearerToken,
[switch]$AllowNotFound
)
$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 20)
$parameters.ContentType = 'application/json'
}
if ($BearerToken) {
$parameters.Headers = @{
Authorization = "Bearer $BearerToken"
}
}
try {
Invoke-RestMethod @parameters
}
catch {
$statusCode = $null
if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
$statusCode = [int]$_.Exception.Response.StatusCode
}
if ($AllowNotFound -and $statusCode -eq 404) {
return $null
}
$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)"
}
}
function Get-AuthToken {
param([string]$Scope)
$token = Invoke-Api -Method POST -Path 'auth/token' -Body @{
clientId = $ClientId
clientSecret = $ClientSecret
scope = $Scope
}
return $token.accessToken
}
function Find-ByName {
param(
[object[]]$Items,
[string]$Name
)
return @($Items | Where-Object { $_.name -eq $Name })[0]
}
function ConvertTo-GuidValue {
param([object]$Value)
return [guid]@($Value)[0]
}
function ConvertTo-ApiItems {
param([object]$Value)
if ($null -eq $Value) {
return @()
}
$items = @($Value)
if ($items.Count -eq 1 -and $items[0] -is [array]) {
return @($items[0])
}
if ($items.Count -eq 1 -and $items[0].PSObject.Properties['value']) {
return @($items[0].value)
}
return $items
}
function Write-TestSection {
param([string]$Name)
Write-Host ""
Write-Host ("=== {0} ===" -f $Name) -ForegroundColor Cyan
}
function Write-TestInfo {
param(
[string]$Label,
[object]$Value
)
Write-Host (" - {0}: {1}" -f $Label, $Value)
}
Describe 'API controller workflow smoke test' {
$script:token = $null
$script:environmentId = $null
$script:domainIds = @()
$script:targetIds = @()
$script:deploymentBatchId = $null
$script:templateSelectionIds = @()
$script:targetAssignmentIds = @()
$script:configurationValueId = $null
$script:credentialSecretId = $null
$script:testRunId = ([guid]::NewGuid().ToString('N')).Substring(0, 8)
$script:environmentName = "Pester Environment $script:testRunId"
$script:domainName1 = "Pester Domain A $script:testRunId"
$script:domainName2 = "Pester Domain B $script:testRunId"
$script:targetName1 = "PST-$script:testRunId-01"
$script:targetName2 = "PST-$script:testRunId-02"
$script:targetName3 = "PST-$script:testRunId-03"
$script:credentialSecretName = "Pester/Workflow/$script:testRunId"
try {
It 'creates an environment, links two domains, creates targets and composes a deployment batch' {
Write-TestSection -Name 'test run'
Write-TestInfo -Label 'API base URL' -Value $ApiBaseUrl
Write-TestInfo -Label 'Run id' -Value $script:testRunId
Write-TestSection -Name 'auth and inventory'
$script:token = Get-AuthToken -Scope 'configuration.read configuration.write credential.read credential.write credential.resolve deployment.read deployment.write queue.read template.read token.manage token.admin'
Write-TestInfo -Label 'Bearer token' -Value 'issued'
$allowedScopesResponse = Invoke-Api -Method GET -Path 'tokens/scopes' -BearerToken $script:token
$allowedScopes = @($allowedScopesResponse)
if ($allowedScopesResponse.PSObject.Properties['value']) {
$allowedScopes = @($allowedScopesResponse.value)
}
$allowedScopes -contains 'deployment.write' | Should Be $true
Write-TestInfo -Label 'Allowed scopes' -Value @($allowedScopes).Count
$myTokens = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'tokens/my' -BearerToken $script:token))
$myTokens.Count | Should BeGreaterThan 0
Write-TestInfo -Label 'Managed tokens visible' -Value $myTokens.Count
$services = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Service' -BearerToken $script:token))
$services.Count | Should BeGreaterThan 0
Write-TestInfo -Label 'Services' -Value $services.Count
$service = $services | Select-Object -First 1
Invoke-Api -Method GET -Path ("Service/{0}" -f $service.id) -BearerToken $script:token | Should Not BeNullOrEmpty
Invoke-Api -Method GET -Path ("Service/{0}/RoleDefinitions" -f $service.id) -BearerToken $script:token | Out-Null
$templateCategories = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'TemplateCategory' -BearerToken $script:token))
$templateCategories.Count | Should BeGreaterThan 0
Write-TestInfo -Label 'Template categories' -Value $templateCategories.Count
Invoke-Api -Method GET -Path ("TemplateCategory/{0}" -f $templateCategories[0].id) -BearerToken $script:token | Should Not BeNullOrEmpty
$deploymentRules = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'deployment-rules' -BearerToken $script:token))
$deploymentRules.Count | Should BeGreaterThan 0
Write-TestInfo -Label 'Deployment rules' -Value $deploymentRules.Count
Invoke-Api -Method GET -Path ("deployment-rules/{0}" -f $deploymentRules[0].id) -BearerToken $script:token | Should Not BeNullOrEmpty
$configurationDefinitions = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'configuration-definitions' -BearerToken $script:token))
$configurationDefinitions.Count | Should BeGreaterThan 0
Write-TestInfo -Label 'Configuration definitions' -Value $configurationDefinitions.Count
$configurationDefinition = $configurationDefinitions | Select-Object -First 1
Invoke-Api -Method GET -Path ("configuration-definitions/{0}" -f $configurationDefinition.id) -BearerToken $script:token | Should Not BeNullOrEmpty
Invoke-Api -Method GET -Path ("configuration-definitions/{0}/values" -f $configurationDefinition.id) -BearerToken $script:token | Out-Null
Write-TestSection -Name 'credential secret'
$script:credentialSecretId = ConvertTo-GuidValue -Value (Invoke-Api -Method POST -Path 'credential-secrets' -BearerToken $script:token -Body @{
name = $script:credentialSecretName
userName = "CONTOSO\svc-pester-$script:testRunId"
secretValue = 'PesterSecret!'
secretType = 'Credential'
metadataJson = (@{ source = 'Api.Workflow.Tests.ps1'; runId = $script:testRunId } | ConvertTo-Json -Compress)
isEnabled = $true
})
Write-TestInfo -Label 'Created credential secret' -Value $script:credentialSecretId
Invoke-Api -Method GET -Path ("credential-secrets/{0}" -f $script:credentialSecretId) -BearerToken $script:token | Should Not BeNullOrEmpty
$resolvedSecret = Invoke-Api -Method GET -Path ("credential-secrets/resolve?name={0}" -f [uri]::EscapeDataString($script:credentialSecretName)) -BearerToken $script:token
$resolvedSecret.userName | Should Be "CONTOSO\svc-pester-$script:testRunId"
Write-TestInfo -Label 'Resolved credential user' -Value $resolvedSecret.userName
Write-TestSection -Name 'environment and domains'
Invoke-Api -Method POST -Path 'Environment' -BearerToken $script:token -Body @{
name = $script:environmentName
environmentType = 'Test'
hostingType = 'OnPrem'
providerType = 'Pester'
metadataJson = (@{ source = 'Api.Workflow.Tests.ps1'; runId = $script:testRunId } | ConvertTo-Json -Compress)
} | Out-Null
$environment = Find-ByName -Items @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Environment' -BearerToken $script:token)) -Name $script:environmentName
$environment | Should Not BeNullOrEmpty
$script:environmentId = ConvertTo-GuidValue -Value $environment.id
Write-TestInfo -Label 'Created environment' -Value ("{0} ({1})" -f $script:environmentName, $script:environmentId)
$script:configurationValueId = ConvertTo-GuidValue -Value (Invoke-Api -Method POST -Path 'configuration-values' -BearerToken $script:token -Body @{
configurationDefinitionId = [string](ConvertTo-GuidValue -Value $configurationDefinition.id)
scopeType = 'Environment'
scopeId = [string]$script:environmentId
valueSourceType = 'Static'
valueJson = (@{ value = "Pester-$script:testRunId" } | ConvertTo-Json -Compress)
sortOrder = 999
})
@(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path ("configuration-values?scopeType=Environment&scopeId={0}" -f $script:environmentId) -BearerToken $script:token)).Count | Should BeGreaterThan 0
Write-TestInfo -Label 'Created configuration value' -Value $script:configurationValueId
$domainId1 = Invoke-Api -Method POST -Path 'Domain' -BearerToken $script:token -Body @{
name = $script:domainName1
fqdn = "pester-a-$script:testRunId.contoso.local"
netBIOS = "PSTA$($script:testRunId.Substring(0, 4))".ToUpper()
}
$domainId2 = Invoke-Api -Method POST -Path 'Domain' -BearerToken $script:token -Body @{
name = $script:domainName2
fqdn = "pester-b-$script:testRunId.contoso.local"
netBIOS = "PSTB$($script:testRunId.Substring(0, 4))".ToUpper()
}
$script:domainIds = @(
(ConvertTo-GuidValue -Value $domainId1),
(ConvertTo-GuidValue -Value $domainId2)
)
Write-TestInfo -Label 'Created domains' -Value ($script:domainIds -join ', ')
foreach ($domainId in $script:domainIds) {
Invoke-Api -Method POST -Path ("Domain/{0}/Environment/{1}" -f $domainId, $script:environmentId) -BearerToken $script:token | Out-Null
}
Write-TestInfo -Label 'Linked domains to environment' -Value $script:domainIds.Count
Write-TestSection -Name 'targets'
$targetDefinitions = @(
@{ name = $script:targetName1; domainId = $script:domainIds[0]; role = 'WebFrontEnd'; order = 10 },
@{ name = $script:targetName2; domainId = $script:domainIds[0]; role = 'Application'; order = 20 },
@{ name = $script:targetName3; domainId = $script:domainIds[1]; role = 'Search'; order = 30 }
)
foreach ($targetDefinition in $targetDefinitions) {
$targetId = Invoke-Api -Method POST -Path 'Target' -BearerToken $script:token -Body @{
domainID = $targetDefinition.domainId
name = $targetDefinition.name
targetType = 'VirtualMachine'
providerType = 'OnPrem'
externalId = $targetDefinition.name
metadataJson = (@{ source = 'Api.Workflow.Tests.ps1'; role = $targetDefinition.role } | ConvertTo-Json -Compress)
}
$script:targetIds += ConvertTo-GuidValue -Value $targetId
Write-TestInfo -Label 'Created target' -Value ("{0} / {1} ({2})" -f $targetDefinition.name, $targetDefinition.role, $targetId)
}
@($script:targetIds).Count | Should Be 3
Write-TestSection -Name 'template selection'
$templates = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Template' -BearerToken $script:token))
$onPremServiceIds = @(
$services |
Where-Object { -not $_.isCloudService } |
ForEach-Object { [string]$_.id }
)
$onPremCategoryIds = @(
$templateCategories |
Where-Object { $onPremServiceIds -contains [string]$_.serviceId } |
ForEach-Object { [string]$_.id }
)
$selectedTemplates = @(
$templates |
Where-Object { $onPremCategoryIds -contains [string]$_.templateCategoryId } |
Select-Object -First 2
)
@($selectedTemplates).Count | Should BeGreaterThan 0
Write-TestInfo -Label 'Available templates' -Value $templates.Count
Write-TestInfo -Label 'Selected templates' -Value (($selectedTemplates | ForEach-Object { $_.name }) -join ', ')
$templateSelections = @()
$sortOrder = 10
foreach ($template in $selectedTemplates) {
$versions = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path ("Template/{0}/Versions" -f $template.id) -BearerToken $script:token))
$version = $versions | Select-Object -First 1
$version | Should Not BeNullOrEmpty
$templateSelections += @{
templateVersionId = [string](ConvertTo-GuidValue -Value $version.id)
templateRole = 'Service'
sortOrder = $sortOrder
alias = "Pester-$sortOrder"
}
Write-TestInfo -Label 'Pinned template version' -Value ("{0} -> {1}" -f $template.name, $version.id)
$sortOrder += 10
}
$targetAssignments = @()
foreach ($targetDefinition in $targetDefinitions) {
$targetId = $script:targetIds[$targetDefinitions.IndexOf($targetDefinition)]
$targetAssignments += @{
targetId = [string]$targetId
roleKey = $targetDefinition.role
sortOrder = $targetDefinition.order
nodeDataJson = (@{ nodeName = $targetDefinition.name; pester = $true } | ConvertTo-Json -Compress)
}
}
Write-TestSection -Name 'deployment batch'
$script:deploymentBatchId = ConvertTo-GuidValue -Value (Invoke-Api -Method POST -Path 'deployment-batches' -BearerToken $script:token -Body @{
status = 'Pending'
targetIds = @()
templateSelections = $templateSelections
targetAssignments = $targetAssignments
})
$script:deploymentBatchId | Should Not BeNullOrEmpty
Write-TestInfo -Label 'Created deployment batch' -Value $script:deploymentBatchId
$composition = Invoke-Api -Method GET -Path ("deployment-batches/{0}/composition" -f $script:deploymentBatchId) -BearerToken $script:token
@($composition.templateSelections).Count | Should Be @($templateSelections).Count
$composition = Invoke-Api -Method GET -Path ("deployment-batches/{0}/composition" -f $script:deploymentBatchId) -BearerToken $script:token
@($composition.targetAssignments).Count | Should Be 3
$script:targetAssignmentIds = @($composition.targetAssignments | ForEach-Object { ConvertTo-GuidValue -Value $_.id })
Write-TestInfo -Label 'Composition template selections' -Value @($composition.templateSelections).Count
Write-TestInfo -Label 'Composition target assignments' -Value @($composition.targetAssignments).Count
$deployments = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Deployment' -BearerToken $script:token) | Where-Object { [string]$_.deploymentGroupId -eq [string]$script:deploymentBatchId })
$deployments.Count | Should Be 3
Write-TestInfo -Label 'Deployment rows' -Value $deployments.Count
Invoke-Api -Method GET -Path ("Deployment/{0}" -f $deployments[0].id) -BearerToken $script:token | Should Not BeNullOrEmpty
Invoke-Api -Method GET -Path ("deployment-artifacts?deploymentGroupId={0}" -f $script:deploymentBatchId) -BearerToken $script:token | Out-Null
Invoke-Api -Method GET -Path 'Deployment/QueueJobs' -BearerToken $script:token | Out-Null
Write-TestInfo -Label 'Artifact and queue endpoints' -Value 'reachable'
}
}
finally {
if ($script:token) {
Write-TestSection -Name 'cleanup'
if ($script:credentialSecretId) {
Invoke-Api -Method DELETE -Path ("credential-secrets/{0}" -f $script:credentialSecretId) -BearerToken $script:token -AllowNotFound | Out-Null
Write-TestInfo -Label 'Deleted credential secret' -Value $script:credentialSecretId
}
if ($script:configurationValueId) {
Invoke-Api -Method DELETE -Path ("configuration-values/{0}" -f $script:configurationValueId) -BearerToken $script:token -AllowNotFound | Out-Null
Write-TestInfo -Label 'Deleted configuration value' -Value $script:configurationValueId
}
foreach ($assignmentId in @($script:targetAssignmentIds)) {
if ($script:deploymentBatchId) {
Invoke-Api -Method DELETE -Path ("deployment-batches/{0}/target-assignments/{1}" -f $script:deploymentBatchId, $assignmentId) -BearerToken $script:token -AllowNotFound | Out-Null
}
}
if ($script:deploymentBatchId) {
Invoke-Api -Method DELETE -Path ("deployment-batches/{0}" -f $script:deploymentBatchId) -BearerToken $script:token -AllowNotFound | Out-Null
Write-TestInfo -Label 'Deleted deployment batch' -Value $script:deploymentBatchId
}
foreach ($targetId in @($script:targetIds)) {
Invoke-Api -Method DELETE -Path ("Target/{0}" -f $targetId) -BearerToken $script:token -AllowNotFound | Out-Null
}
if (@($script:targetIds).Count -gt 0) {
Write-TestInfo -Label 'Deleted targets' -Value @($script:targetIds).Count
}
foreach ($domainId in @($script:domainIds)) {
if ($script:environmentId) {
Invoke-Api -Method DELETE -Path ("Domain/{0}/Environment/{1}" -f $domainId, $script:environmentId) -BearerToken $script:token -AllowNotFound | Out-Null
}
Invoke-Api -Method DELETE -Path ("Domain/{0}" -f $domainId) -BearerToken $script:token -AllowNotFound | Out-Null
}
if (@($script:domainIds).Count -gt 0) {
Write-TestInfo -Label 'Deleted domains' -Value @($script:domainIds).Count
}
if ($script:environmentId) {
Invoke-Api -Method DELETE -Path ("Environment/{0}" -f $script:environmentId) -BearerToken $script:token -AllowNotFound | Out-Null
Write-TestInfo -Label 'Deleted environment' -Value $script:environmentId
}
}
}
}

View File

@@ -66,25 +66,69 @@ function Invoke-TestApi {
}
}
function Invoke-TestApiExpectFailure {
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 | Out-Null
throw "API request was expected to fail but succeeded. Method=[$Method], Uri=[$uri]."
}
catch {
if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
return [int]$_.Exception.Response.StatusCode
}
throw
}
}
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'
scope = 'configuration.read'
}
$token.accessToken | Should Not BeNullOrEmpty
$token.tokenType | Should Be 'Bearer'
$token.expiresIn | Should BeGreaterThan 0
$token.scope | Should Be 'deployment.read template.read'
$token.scope | Should Be 'configuration.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'
scope = 'configuration.read'
}
$definitions = @(Invoke-TestApi -Method GET -Path 'configuration-definitions?kind=Parameter' -BearerToken $token.accessToken)
@@ -93,6 +137,18 @@ Describe 'On-prem client credentials API authentication' {
@($definitions | Where-Object { $_.kind -ne 'Parameter' }).Count | Should Be 0
}
It 'rejects bearer-token API calls without the required scope' {
$token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
clientId = $ClientId
clientSecret = $ClientSecret
scope = 'deployment.read'
}
$statusCode = Invoke-TestApiExpectFailure -Method GET -Path 'configuration-definitions?kind=Parameter' -BearerToken $token.accessToken
@(401, 403) -contains $statusCode | Should Be $true
}
It 'creates, lists and revokes managed tokens through the token API' {
$adminToken = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
clientId = $ClientId

View File

@@ -0,0 +1,19 @@
namespace Microsoft.SelfService.Portal.Core.API.Authorization
{
public static class ApiPolicies
{
public const string ConfigurationRead = "ConfigurationRead";
public const string ConfigurationWrite = "ConfigurationWrite";
public const string CredentialRead = "CredentialRead";
public const string CredentialWrite = "CredentialWrite";
public const string CredentialResolve = "CredentialResolve";
public const string DeploymentRead = "DeploymentRead";
public const string DeploymentWrite = "DeploymentWrite";
public const string QueueRead = "QueueRead";
public const string QueueProcess = "QueueProcess";
public const string TemplateRead = "TemplateRead";
public const string TemplateWrite = "TemplateWrite";
public const string TokenManage = "TokenManage";
public const string TokenAdmin = "TokenAdmin";
}
}

View File

@@ -0,0 +1,19 @@
namespace Microsoft.SelfService.Portal.Core.API.Authorization
{
public static class ApiScopes
{
public const string ConfigurationRead = "configuration.read";
public const string ConfigurationWrite = "configuration.write";
public const string CredentialRead = "credential.read";
public const string CredentialWrite = "credential.write";
public const string CredentialResolve = "credential.resolve";
public const string DeploymentRead = "deployment.read";
public const string DeploymentWrite = "deployment.write";
public const string QueueRead = "queue.read";
public const string QueueProcess = "queue.process";
public const string TemplateRead = "template.read";
public const string TemplateWrite = "template.write";
public const string TokenManage = "token.manage";
public const string TokenAdmin = "token.admin";
}
}

View File

@@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Authorization;
namespace Microsoft.SelfService.Portal.Core.API.Authorization
{
public static class AuthorizationServiceCollectionExtensions
{
public static IServiceCollection AddSelfServicePortalAuthorization(this IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
AddScopePolicy(options, ApiPolicies.ConfigurationRead, ApiScopes.ConfigurationRead, ApiScopes.ConfigurationWrite);
AddScopePolicy(options, ApiPolicies.ConfigurationWrite, ApiScopes.ConfigurationWrite);
AddScopePolicy(options, ApiPolicies.CredentialRead, ApiScopes.CredentialRead, ApiScopes.CredentialWrite, ApiScopes.CredentialResolve);
AddScopePolicy(options, ApiPolicies.CredentialWrite, ApiScopes.CredentialWrite);
AddScopePolicy(options, ApiPolicies.CredentialResolve, ApiScopes.CredentialResolve);
AddScopePolicy(options, ApiPolicies.DeploymentRead, ApiScopes.DeploymentRead, ApiScopes.DeploymentWrite);
AddScopePolicy(options, ApiPolicies.DeploymentWrite, ApiScopes.DeploymentWrite);
AddScopePolicy(options, ApiPolicies.QueueRead, ApiScopes.QueueRead, ApiScopes.QueueProcess);
AddScopePolicy(options, ApiPolicies.QueueProcess, ApiScopes.QueueProcess);
AddScopePolicy(options, ApiPolicies.TemplateRead, ApiScopes.TemplateRead, ApiScopes.TemplateWrite);
AddScopePolicy(options, ApiPolicies.TemplateWrite, ApiScopes.TemplateWrite);
AddScopePolicy(options, ApiPolicies.TokenManage, ApiScopes.TokenManage, ApiScopes.TokenAdmin);
AddScopePolicy(options, ApiPolicies.TokenAdmin, ApiScopes.TokenAdmin);
});
return services;
}
private static void AddScopePolicy(AuthorizationOptions options, string policyName, params string[] acceptedScopes)
{
options.AddPolicy(policyName, policy => policy.RequireAssertion(context =>
{
if (context.User.Identity?.IsAuthenticated != true)
{
return false;
}
var tokenScopes = context.User.FindAll("scope").Select(claim => claim.Value).ToList();
if (tokenScopes.Count == 0)
{
return true;
}
return acceptedScopes.Any(scope => tokenScopes.Contains(scope, StringComparer.OrdinalIgnoreCase));
}));
}
}
}

View File

@@ -364,7 +364,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Context
ClientId = "ssp-demo-worker",
Name = "Demo Worker Client",
SecretHash = ApiClientSecretHasher.HashSecretForDemoData("DemoOnly-DoNotUseInProduction!", "ssp-demo-worker"),
ScopesJson = "[\"deployment.read\",\"deployment.write\",\"queue.process\",\"template.read\",\"credential.resolve\",\"token.manage\",\"token.admin\"]",
ScopesJson = "[\"configuration.read\",\"configuration.write\",\"credential.read\",\"credential.write\",\"credential.resolve\",\"deployment.read\",\"deployment.write\",\"queue.read\",\"queue.process\",\"template.read\",\"template.write\",\"token.manage\",\"token.admin\"]",
IsEnabled = true
})
];

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.Auth;
using Microsoft.SelfService.Portal.Core.API.Models;
@@ -24,7 +25,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpGet("my")]
[Authorize(Policy = "TokenManage")]
[Authorize(Policy = ApiPolicies.TokenManage)]
[ProducesResponseType(200, Type = typeof(IEnumerable<GetManagedTokenDto>))]
public IActionResult GetMyTokens()
{
@@ -41,7 +42,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("my")]
[Authorize(Policy = "TokenManage")]
[Authorize(Policy = ApiPolicies.TokenManage)]
[ProducesResponseType(200, Type = typeof(CreateManagedTokenResponseDto))]
[ProducesResponseType(400)]
public IActionResult CreateMyToken([FromBody] CreateManagedTokenRequestDto request)
@@ -111,7 +112,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("my/{id}")]
[Authorize(Policy = "TokenManage")]
[Authorize(Policy = ApiPolicies.TokenManage)]
[ProducesResponseType(200, Type = typeof(GetManagedTokenDto))]
[ProducesResponseType(404)]
public IActionResult RevokeMyToken(Guid id)
@@ -135,7 +136,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpGet("admin")]
[Authorize(Policy = "TokenAdmin")]
[Authorize(Policy = ApiPolicies.TokenAdmin)]
[ProducesResponseType(200, Type = typeof(IEnumerable<GetManagedTokenDto>))]
public IActionResult GetAllTokens()
{
@@ -148,7 +149,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("admin/{id}")]
[Authorize(Policy = "TokenAdmin")]
[Authorize(Policy = ApiPolicies.TokenAdmin)]
[ProducesResponseType(200, Type = typeof(GetManagedTokenDto))]
[ProducesResponseType(404)]
public IActionResult RevokeTokenAsAdmin(Guid id)
@@ -166,7 +167,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpGet("scopes")]
[Authorize(Policy = "TokenManage")]
[Authorize(Policy = ApiPolicies.TokenManage)]
[ProducesResponseType(200, Type = typeof(IEnumerable<string>))]
public IActionResult GetAllowedScopes()
{

View File

@@ -1,6 +1,8 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationDefinition.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationDefinition.Edit;
@@ -13,6 +15,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/configuration-definitions")]
[ApiController]
[Authorize(Policy = ApiPolicies.ConfigurationRead)]
public class ConfigurationDefinitionController : Controller
{
private readonly DataContext _context;
@@ -84,6 +87,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(422)]
@@ -126,6 +130,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -173,6 +178,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
[ProducesResponseType(409)]

View File

@@ -1,6 +1,8 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationValue.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationValue.Get;
@@ -11,6 +13,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/configuration-values")]
[ApiController]
[Authorize(Policy = ApiPolicies.ConfigurationRead)]
public class ConfigurationValueController : Controller
{
private readonly DataContext _context;
@@ -50,6 +53,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
public IActionResult AddConfigurationValue([FromBody] AddConfigurationValueDto configurationValue)
@@ -89,6 +93,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteConfigurationValue(Guid id)

View File

@@ -1,6 +1,8 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.CredentialSecret.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.CredentialSecret.Get;
@@ -11,6 +13,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/credential-secrets")]
[ApiController]
[Authorize(Policy = ApiPolicies.CredentialRead)]
public class CredentialSecretController : Controller
{
private readonly DataContext _context;
@@ -50,6 +53,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpGet("resolve")]
[Authorize(Policy = ApiPolicies.CredentialResolve)]
[ProducesResponseType(200, Type = typeof(GetCredentialSecretValueDto))]
[ProducesResponseType(404)]
public IActionResult ResolveCredentialSecret([FromQuery] string name)
@@ -65,6 +69,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.CredentialWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
public IActionResult AddCredentialSecret([FromBody] AddCredentialSecretDto credentialSecret)
@@ -103,6 +108,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{id}")]
[Authorize(Policy = ApiPolicies.CredentialWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult UpdateCredentialSecret(Guid id, [FromBody] AddCredentialSecretDto credentialSecret)
@@ -126,6 +132,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}")]
[Authorize(Policy = ApiPolicies.CredentialWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteCredentialSecret(Guid id)

View File

@@ -1,6 +1,8 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentArtifact.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentArtifact.Get;
@@ -12,6 +14,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/deployment-artifacts")]
[ApiController]
[Authorize(Policy = ApiPolicies.DeploymentRead)]
public class DeploymentArtifactController : Controller
{
private readonly DataContext _context;
@@ -60,6 +63,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
public IActionResult AddDeploymentArtifact([FromBody] AddDeploymentArtifactDto deploymentArtifact)
@@ -120,6 +124,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{id}/stale-check")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(GetDeploymentArtifactDto))]
[ProducesResponseType(404)]
public IActionResult CheckDeploymentArtifactStale(Guid id)

View File

@@ -1,6 +1,8 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentBatch.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentBatch.Edit;
@@ -15,6 +17,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/deployment-batches")]
[ApiController]
[Authorize(Policy = ApiPolicies.DeploymentRead)]
public class DeploymentBatchController : Controller
{
private readonly IDeploymentBatchInterface _deploymentBatchInterface;
@@ -82,6 +85,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
public IActionResult AddDeploymentBatch([FromBody] AddDeploymentBatchDto deploymentBatch)
@@ -118,6 +122,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -144,6 +149,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -182,6 +188,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{id}/template-selections")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -233,6 +240,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}/template-selections/{selectionId}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteTemplateSelection(Guid id, Guid selectionId)
@@ -250,6 +258,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{id}/parameter-values")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -299,6 +308,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}/parameter-values/{parameterValueId}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteParameterValue(Guid id, Guid parameterValueId)
@@ -316,6 +326,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{id}/target-assignments")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -367,6 +378,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}/target-assignments/{targetAssignmentId}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteTargetAssignment(Guid id, Guid targetAssignmentId)

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Dto.Deployment.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.Deployment.Get;
using Microsoft.SelfService.Portal.Core.API.Dto.Deployment.Edit;
@@ -13,6 +15,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = ApiPolicies.DeploymentRead)]
public class DeploymentController : Controller
{
private readonly IDeploymentInterface _deploymentInterface;
@@ -62,6 +65,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
public IActionResult AddDeploymentById([FromBody] AddDeploymentDto deployment)
@@ -93,6 +97,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("Request")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
public IActionResult AddDeploymentRequest([FromBody] AddDeploymentRequestDto request)
@@ -234,6 +239,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("QueueJobs/{Id}/Retry")]
[Authorize(Policy = ApiPolicies.QueueProcess)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult RetryQueueJob(Guid Id)
@@ -245,6 +251,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("QueueJobs/Steps/{stepId}/Approve")]
[Authorize(Policy = ApiPolicies.QueueProcess)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult ApproveQueueJobStep(Guid stepId, [FromBody] QueueJobStepApprovalDto? payload)
@@ -257,6 +264,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("QueueJobs/Steps/{stepId}/Reject")]
[Authorize(Policy = ApiPolicies.QueueProcess)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult RejectQueueJobStep(Guid stepId, [FromBody] QueueJobStepApprovalDto? payload)
@@ -269,6 +277,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -291,6 +300,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("Batch/{deploymentBatchId}/Target/{targetId}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -317,6 +327,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]

View File

@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentRule.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentRule.Get;
@@ -9,6 +11,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/deployment-rules")]
[ApiController]
[Authorize(Policy = ApiPolicies.DeploymentRead)]
public class DeploymentRuleController : Controller
{
private readonly DataContext _context;
@@ -49,6 +52,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
public IActionResult AddDeploymentRule([FromBody] AddDeploymentRuleDto deploymentRule)
@@ -84,6 +88,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult EditDeploymentRule(Guid id, [FromBody] AddDeploymentRuleDto deploymentRule)
@@ -123,6 +128,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteDeploymentRule(Guid id)
@@ -181,4 +187,3 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
}
}

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Dto.Domain.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.Domain.Edit;
using Microsoft.SelfService.Portal.Core.API.Dto.Domain.Get;
@@ -10,6 +12,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = ApiPolicies.ConfigurationRead)]
public class DomainController : Controller
{
private readonly IDomainInterface _domainInterface;
@@ -53,6 +56,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
public IActionResult AddDomainById([FromBody] AddDomainDto domain)
@@ -83,6 +87,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -105,6 +110,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -149,6 +155,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{DomainId}/Environment/{EnvironmentId}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200, Type = typeof(EnvironmentDomainsModel))]
[ProducesResponseType(400)]
public IActionResult LinkDomainByIdToEnvironment(Guid DomainId, Guid EnvironmentId)
@@ -183,6 +190,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{DomainId}/Environment/{EnvironmentId}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200, Type = typeof(EnvironmentDomainsModel))]
[ProducesResponseType(400)]
public IActionResult UnlinkDomainByIdFromEnvironment(Guid DomainId, Guid EnvironmentId)

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Dto.Environment.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.Environment.Edit;
using Microsoft.SelfService.Portal.Core.API.Dto.Environment.Get;
@@ -12,6 +14,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = ApiPolicies.ConfigurationRead)]
public class EnvironmentController : Controller
{
private readonly IEnvironmentInterface _environmentInterface;
@@ -53,6 +56,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
public IActionResult AddEnvironmentById([FromBody] AddEnvironmentDto environment)
@@ -88,6 +92,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -110,6 +115,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Dto.Service.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.Service.Edit;
using Microsoft.SelfService.Portal.Core.API.Dto.Service.Get;
@@ -11,6 +13,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = ApiPolicies.ConfigurationRead)]
public class ServiceController : Controller
{
private readonly IMapper _mapper;
@@ -51,6 +54,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
public IActionResult AddServiceById([FromBody] AddServiceDto service)
@@ -79,6 +83,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -106,6 +111,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -141,6 +147,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{Id}/RoleDefinitions")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -177,6 +184,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}/RoleDefinitions/{RoleDefinitionId}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -216,6 +224,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}/RoleDefinitions/{RoleDefinitionId}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteRoleDefinition(Guid Id, Guid RoleDefinitionId)

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Dto.Target.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.Target.Edit;
using Microsoft.SelfService.Portal.Core.API.Dto.Target.Get;
@@ -10,6 +12,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = ApiPolicies.ConfigurationRead)]
public class TargetController : Controller
{
private readonly ITargetInterface _targetInterface;
@@ -52,6 +55,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
public IActionResult AddTargetById([FromBody] AddTargetDto target)
@@ -86,6 +90,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -120,6 +125,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{Id}/Domain/{domainId}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -151,6 +157,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}/Domain")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult UnlinkTargetFromDomain(Guid Id)
@@ -177,6 +184,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Dto.TemplateCategory.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.TemplateCategory.Edit;
using Microsoft.SelfService.Portal.Core.API.Dto.TemplateCategory.Get;
@@ -10,6 +12,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = ApiPolicies.TemplateRead)]
public class TemplateCategoryController : Controller
{
private readonly IMapper _mapper;
@@ -50,6 +53,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
public IActionResult AddTemplateCategoryById([FromBody] AddTemplateCategoryDto templateCategory)
@@ -78,6 +82,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}")]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -105,6 +110,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}")]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Dto.Template.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.Template.Edit;
using Microsoft.SelfService.Portal.Core.API.Dto.Template.Get;
@@ -16,6 +18,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = ApiPolicies.TemplateRead)]
public class TemplateController : Controller
{
private readonly ITemplateInterface _templateInterface;
@@ -61,6 +64,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
public IActionResult AddTemplateById([FromBody] AddTemplateDto template)
@@ -97,6 +101,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPut("{Id}")]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -139,6 +144,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpDelete("{Id}")]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -186,6 +192,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{Id}/Versions")]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -262,6 +269,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{Id}/Versions/{VersionId}/Revisions")]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
@@ -298,6 +306,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
}
[HttpPost("{Id}/Versions/{VersionId}/Publish")]
[Authorize(Policy = ApiPolicies.TemplateWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult PublishTemplateVersion(Guid Id, Guid VersionId)

View File

@@ -160,6 +160,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Helper
/** Credential Secret Model **/
CreateMap<CredentialSecretModel, GetCredentialSecretDto>();
CreateMap<CredentialSecretModel, GetCredentialSecretValueDto>();
CreateMap<AddCredentialSecretDto, CredentialSecretModel>();
/** Service Model **/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Microsoft.SelfService.Portal.Core.API.Migrations
{
/// <inheritdoc />
public partial class AddCentralizedAuthorizationScopes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "ApiClients",
keyColumn: "Id",
keyValue: new Guid("91000000-0000-0000-0000-000000000001"),
column: "ScopesJson",
value: "[\"configuration.read\",\"configuration.write\",\"credential.read\",\"credential.write\",\"credential.resolve\",\"deployment.read\",\"deployment.write\",\"queue.read\",\"queue.process\",\"template.read\",\"template.write\",\"token.manage\",\"token.admin\"]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "ApiClients",
keyColumn: "Id",
keyValue: new Guid("91000000-0000-0000-0000-000000000001"),
column: "ScopesJson",
value: "[\"deployment.read\",\"deployment.write\",\"queue.process\",\"template.read\",\"credential.resolve\",\"token.manage\",\"token.admin\"]");
}
}
}

View File

@@ -106,7 +106,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Migrations
Modified = new DateTime(2026, 7, 7, 0, 0, 0, 0, DateTimeKind.Utc),
ModifiedBy = "DemoSeed",
Name = "Demo Worker Client",
ScopesJson = "[\"deployment.read\",\"deployment.write\",\"queue.process\",\"template.read\",\"credential.resolve\",\"token.manage\",\"token.admin\"]",
ScopesJson = "[\"configuration.read\",\"configuration.write\",\"credential.read\",\"credential.write\",\"credential.resolve\",\"deployment.read\",\"deployment.write\",\"queue.read\",\"queue.process\",\"template.read\",\"template.write\",\"token.manage\",\"token.admin\"]",
SecretHash = "PBKDF2-SHA256.100000.c3NwLWRlbW8td29ya2VyAA==.xQxe7BHCn9pdkqHozdyspEmKnz95uCUuxVvU2vgCEbo="
});
});

View File

@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Interfaces;
using Microsoft.SelfService.Portal.Core.API.Repository;
@@ -109,20 +110,7 @@ builder.Services.AddAuthentication(options =>
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy.
options.FallbackPolicy = options.DefaultPolicy;
options.AddPolicy("QueueProcess", policy => policy.RequireClaim("scope", "queue.process"));
options.AddPolicy("DeploymentRead", policy => policy.RequireClaim("scope", "deployment.read"));
options.AddPolicy("CredentialResolve", policy => policy.RequireClaim("scope", "credential.resolve"));
options.AddPolicy("TokenManage", policy => policy.RequireAssertion(context =>
context.User.HasClaim("scope", "token.manage")
|| context.User.Identity?.AuthenticationType == NegotiateDefaults.AuthenticationScheme));
options.AddPolicy("TokenAdmin", policy => policy.RequireAssertion(context =>
context.User.HasClaim("scope", "token.admin")
|| context.User.Identity?.AuthenticationType == NegotiateDefaults.AuthenticationScheme));
});
builder.Services.AddSelfServicePortalAuthorization();
var app = builder.Build();
var frontendDistPath = Path.GetFullPath(Path.Combine(

View File

@@ -3,6 +3,7 @@ using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
using Microsoft.SelfService.Portal.Core.API.Authorization;
using Microsoft.SelfService.Portal.Core.API.Models;
namespace Microsoft.SelfService.Portal.Core.API.Services
@@ -138,13 +139,19 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
return
[
"deployment.read",
"deployment.write",
"queue.process",
"template.read",
"credential.resolve",
"token.manage",
"token.admin"
ApiScopes.ConfigurationRead,
ApiScopes.ConfigurationWrite,
ApiScopes.CredentialRead,
ApiScopes.CredentialWrite,
ApiScopes.CredentialResolve,
ApiScopes.DeploymentRead,
ApiScopes.DeploymentWrite,
ApiScopes.QueueRead,
ApiScopes.QueueProcess,
ApiScopes.TemplateRead,
ApiScopes.TemplateWrite,
ApiScopes.TokenManage,
ApiScopes.TokenAdmin
];
}

View File

@@ -15,8 +15,14 @@
"AllowedTokenScopes": [
"deployment.read",
"deployment.write",
"configuration.read",
"configuration.write",
"credential.read",
"credential.write",
"queue.process",
"queue.read",
"template.read",
"template.write",
"credential.resolve",
"token.manage",
"token.admin"