Files
Microsoft.SelfService.Porta…/Controllers/ConfigurationDefinitionController.cs
Torsten Brendgen dfd9c16f61 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.
2026-07-09 23:49:01 +02:00

273 lines
11 KiB
C#

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;
using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationDefinition.Get;
using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationValue.Get;
using Microsoft.SelfService.Portal.Core.API.JsonDocuments;
using Microsoft.SelfService.Portal.Core.API.Models;
namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/configuration-definitions")]
[ApiController]
[Authorize(Policy = ApiPolicies.ConfigurationRead)]
public class ConfigurationDefinitionController : Controller
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public ConfigurationDefinitionController(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
[HttpGet]
[ProducesResponseType(200, Type = typeof(IEnumerable<GetConfigurationDefinitionDto>))]
public IActionResult GetConfigurationDefinitions([FromQuery] Guid? templateRevisionId, [FromQuery] string? kind, [FromQuery] string? name)
{
var query = _context.ConfigurationDefinitions
.AsNoTracking()
.AsQueryable();
if (templateRevisionId.HasValue)
query = query.Where(definition => definition.TemplateRevisionId == templateRevisionId.Value);
if (!string.IsNullOrWhiteSpace(kind))
query = query.Where(definition => definition.Kind == kind);
if (!string.IsNullOrWhiteSpace(name))
query = query.Where(definition => definition.Name == name);
var definitions = query
.OrderBy(definition => definition.TemplateRevisionId)
.ThenBy(definition => definition.Kind)
.ThenBy(definition => definition.Name)
.ToList();
return Ok(_mapper.Map<List<GetConfigurationDefinitionDto>>(definitions));
}
[HttpGet("{id}")]
[ProducesResponseType(200, Type = typeof(GetConfigurationDefinitionDto))]
[ProducesResponseType(404)]
public IActionResult GetConfigurationDefinitionById(Guid id)
{
var definition = _context.ConfigurationDefinitions
.AsNoTracking()
.FirstOrDefault(existing => existing.Id == id);
if (definition == null)
return NotFound();
return Ok(_mapper.Map<GetConfigurationDefinitionDto>(definition));
}
[HttpGet("{id}/values")]
[ProducesResponseType(200, Type = typeof(IEnumerable<GetConfigurationValueDto>))]
[ProducesResponseType(404)]
public IActionResult GetConfigurationDefinitionValues(Guid id)
{
if (!_context.ConfigurationDefinitions.Any(definition => definition.Id == id))
return NotFound();
var values = _context.ConfigurationValues
.AsNoTracking()
.Where(value => value.ConfigurationDefinitionId == id)
.OrderBy(value => value.ScopeType)
.ThenBy(value => value.SortOrder)
.ToList();
return Ok(_mapper.Map<List<GetConfigurationValueDto>>(values));
}
[HttpPost]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(422)]
public IActionResult AddConfigurationDefinition([FromBody] AddConfigurationDefinitionDto configurationDefinition)
{
if (configurationDefinition == null)
return BadRequest(ModelState);
if (!TryPrepareConfigurationDefinition(
configurationDefinition.TemplateRevisionId,
configurationDefinition.Kind,
configurationDefinition.Name,
configurationDefinition.PropertiesJson,
null,
out var propertiesJson,
out var errorField,
out var errorMessage))
{
ModelState.AddModelError(errorField, errorMessage);
return BadRequest(ModelState);
}
if (ConfigurationDefinitionExists(configurationDefinition.TemplateRevisionId, configurationDefinition.Kind, configurationDefinition.Name, null))
{
ModelState.AddModelError(nameof(configurationDefinition.Name), "Configuration definition already exists for this scope, kind and name.");
return StatusCode(422, ModelState);
}
var model = _mapper.Map<ConfigurationDefinitionModel>(configurationDefinition);
model.Id = Guid.NewGuid();
model.PropertiesJson = propertiesJson;
model.DefinitionHash = TemplateVersionModel.ComputeSha256(model.PropertiesJson);
model.CreatedBy = User?.Identity?.Name ?? "System";
model.ModifiedBy = model.CreatedBy;
_context.ConfigurationDefinitions.Add(model);
_context.SaveChanges();
return Ok(model.Id);
}
[HttpPut("{id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(422)]
public IActionResult UpdateConfigurationDefinition(Guid id, [FromBody] EditConfigurationDefinitionDto configurationDefinition)
{
if (configurationDefinition == null)
return BadRequest(ModelState);
var existing = _context.ConfigurationDefinitions.FirstOrDefault(definition => definition.Id == id);
if (existing == null)
return NotFound();
if (!TryPrepareConfigurationDefinition(
configurationDefinition.TemplateRevisionId,
configurationDefinition.Kind,
configurationDefinition.Name,
configurationDefinition.PropertiesJson,
id,
out var propertiesJson,
out var errorField,
out var errorMessage))
{
ModelState.AddModelError(errorField, errorMessage);
return BadRequest(ModelState);
}
if (ConfigurationDefinitionExists(configurationDefinition.TemplateRevisionId, configurationDefinition.Kind, configurationDefinition.Name, id))
{
ModelState.AddModelError(nameof(configurationDefinition.Name), "Configuration definition already exists for this scope, kind and name.");
return StatusCode(422, ModelState);
}
existing.TemplateRevisionId = configurationDefinition.TemplateRevisionId;
existing.Kind = configurationDefinition.Kind;
existing.Name = configurationDefinition.Name;
existing.PropertiesJson = propertiesJson;
existing.DefinitionHash = TemplateVersionModel.ComputeSha256(propertiesJson);
existing.Modified = DateTime.UtcNow;
existing.ModifiedBy = User?.Identity?.Name ?? "System";
_context.SaveChanges();
return NoContent();
}
[HttpDelete("{id}")]
[Authorize(Policy = ApiPolicies.ConfigurationWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
[ProducesResponseType(409)]
public IActionResult DeleteConfigurationDefinition(Guid id)
{
var definition = _context.ConfigurationDefinitions
.Include(existing => existing.Values)
.FirstOrDefault(existing => existing.Id == id);
if (definition == null)
return NotFound();
if (definition.Values.Any())
{
ModelState.AddModelError(nameof(id), "Configuration definition cannot be deleted while configuration values reference it.");
return StatusCode(409, ModelState);
}
_context.ConfigurationDefinitions.Remove(definition);
_context.SaveChanges();
return NoContent();
}
private bool TryPrepareConfigurationDefinition(
Guid? templateRevisionId,
string kind,
string name,
string propertiesJson,
Guid? existingId,
out string normalizedPropertiesJson,
out string errorField,
out string errorMessage)
{
normalizedPropertiesJson = "{}";
errorField = string.Empty;
errorMessage = string.Empty;
if (templateRevisionId.HasValue && !_context.TemplateRevisions.Any(revision => revision.Id == templateRevisionId.Value))
{
errorField = nameof(templateRevisionId);
errorMessage = "Template revision was not found.";
return false;
}
if (!string.Equals(kind, ConfigurationDefinitionKinds.Parameter, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(kind, ConfigurationDefinitionKinds.Variable, StringComparison.OrdinalIgnoreCase))
{
errorField = nameof(kind);
errorMessage = $"Configuration definition kind must be [{ConfigurationDefinitionKinds.Parameter}] or [{ConfigurationDefinitionKinds.Variable}].";
return false;
}
if (string.IsNullOrWhiteSpace(name))
{
errorField = nameof(name);
errorMessage = "Configuration definition name is required.";
return false;
}
try
{
normalizedPropertiesJson = ConfigurationDocumentValidator.NormalizeAndValidate(
propertiesJson,
ConfigurationDocumentKind.Metadata).JsonData;
}
catch (ConfigurationDocumentValidationException ex)
{
errorField = nameof(propertiesJson);
errorMessage = ex.Message;
return false;
}
return true;
}
private bool ConfigurationDefinitionExists(Guid? templateRevisionId, string kind, string name, Guid? excludeId)
{
var query = _context.ConfigurationDefinitions.AsQueryable();
if (excludeId.HasValue)
query = query.Where(definition => definition.Id != excludeId.Value);
query = templateRevisionId.HasValue
? query.Where(definition => definition.TemplateRevisionId == templateRevisionId.Value)
: query.Where(definition => definition.TemplateRevisionId == null);
return query.Any(definition => definition.Kind == kind && definition.Name == name);
}
}
}