Files
Microsoft.SelfService.Porta…/Controllers/DeploymentBatchController.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

448 lines
18 KiB
C#

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;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentBatch.Get;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentComposition.Add;
using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentComposition.Get;
using Microsoft.SelfService.Portal.Core.API.Interfaces;
using Microsoft.SelfService.Portal.Core.API.JsonDocuments;
using Microsoft.SelfService.Portal.Core.API.Models;
namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/deployment-batches")]
[ApiController]
[Authorize(Policy = ApiPolicies.DeploymentRead)]
public class DeploymentBatchController : Controller
{
private readonly IDeploymentBatchInterface _deploymentBatchInterface;
private readonly DataContext _context;
private readonly IMapper _mapper;
public DeploymentBatchController(IDeploymentBatchInterface deploymentBatchInterface, DataContext context, IMapper mapper)
{
_deploymentBatchInterface = deploymentBatchInterface;
_context = context;
_mapper = mapper;
}
[HttpGet]
[ProducesResponseType(200, Type = typeof(IEnumerable<GetDeploymentBatchDto>))]
public IActionResult GetDeploymentBatches()
{
var deploymentBatches = _deploymentBatchInterface
.GetDeploymentBatches()
.Select(batch =>
{
var primarySelection = batch.TemplateSelections
.OrderBy(selection => selection.SortOrder)
.FirstOrDefault();
return new GetDeploymentBatchDto
{
Id = batch.Id,
TemplateId = batch.TemplateId,
DeploymentRuleId = batch.DeploymentRuleId,
Status = batch.Status,
Created = batch.Created,
CreatedBy = batch.CreatedBy,
Modified = batch.Modified,
ModifiedBy = batch.ModifiedBy,
PrimaryTemplateVersionId = primarySelection?.TemplateVersionId,
PrimaryTemplateName = primarySelection?.TemplateVersion?.Template?.Name,
PrimaryTemplateVersion = primarySelection?.TemplateVersion?.Version,
TemplateSelectionCount = batch.TemplateSelections.Count,
TargetAssignmentCount = batch.TargetAssignments.Count
};
})
.ToList();
if (!ModelState.IsValid)
return BadRequest(ModelState);
return Ok(deploymentBatches);
}
[HttpGet("{id}")]
[ProducesResponseType(200, Type = typeof(DeploymentGroupModel))]
[ProducesResponseType(400)]
public IActionResult GetDeploymentBatchById(Guid id)
{
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
return NotFound();
var deploymentBatch = _mapper.Map<GetDeploymentBatchDetailsDto>(_deploymentBatchInterface.GetDeploymentBatchById(id));
if (!ModelState.IsValid)
return BadRequest(ModelState);
return Ok(deploymentBatch);
}
[HttpPost]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
public IActionResult AddDeploymentBatch([FromBody] AddDeploymentBatchDto deploymentBatch)
{
if (deploymentBatch == null)
return BadRequest(ModelState);
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (deploymentBatch.TemplateVersionId.HasValue
&& (deploymentBatch.TemplateSelections == null || deploymentBatch.TemplateSelections.Count == 0))
{
deploymentBatch.TemplateSelections = new List<AddDeploymentTemplateSelectionDto>
{
new()
{
TemplateVersionId = deploymentBatch.TemplateVersionId.Value,
TemplateRole = "Service",
SortOrder = 10
}
};
}
var deploymentBatchMap = _mapper.Map<DeploymentGroupModel>(deploymentBatch);
if (!_deploymentBatchInterface.AddDeploymentBatchById(deploymentBatchMap, deploymentBatch.TargetIds))
{
ModelState.AddModelError("", "Something went wrong while saving");
return StatusCode(500, ModelState);
}
return Ok(deploymentBatchMap.Id);
}
[HttpDelete("{id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public IActionResult DeleteDeploymentBatchById(Guid id)
{
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
return NotFound();
if (!ModelState.IsValid)
return BadRequest(ModelState);
var deploymentBatch = _deploymentBatchInterface.GetDeploymentBatchById(id);
if (deploymentBatch == null)
return NotFound();
if (!_deploymentBatchInterface.DeleteDeploymentBatchById(deploymentBatch))
{
ModelState.AddModelError("", "Something went wrong while deleting");
return StatusCode(500, ModelState);
}
return NoContent();
}
[HttpPut("{id}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public IActionResult EditDeploymentBatchById(Guid id, [FromBody] EditDeploymentBatchDto deploymentBatch)
{
if (deploymentBatch == null)
return BadRequest(ModelState);
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
return NotFound();
if (!ModelState.IsValid)
return BadRequest(ModelState);
var deploymentBatchMap = _mapper.Map<DeploymentGroupModel>(deploymentBatch);
deploymentBatchMap.Id = id;
if (!_deploymentBatchInterface.EditDeploymentBatchById(deploymentBatchMap))
{
ModelState.AddModelError("", "Something went wrong");
return StatusCode(500, ModelState);
}
return NoContent();
}
[HttpGet("{id}/composition")]
[ProducesResponseType(200, Type = typeof(GetDeploymentCompositionDto))]
[ProducesResponseType(404)]
public IActionResult GetDeploymentComposition(Guid id)
{
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
return NotFound();
return Ok(LoadComposition(id));
}
[HttpPost("{id}/template-selections")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public IActionResult AddTemplateSelection(Guid id, [FromBody] AddDeploymentTemplateSelectionDto templateSelection)
{
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
return NotFound();
var templateVersion = _context.TemplateVersions
.Include(version => version.TemplateRevisions)
.FirstOrDefault(version => version.Id == templateSelection.TemplateVersionId);
if (templateVersion == null)
{
ModelState.AddModelError(nameof(templateSelection.TemplateVersionId), "Template version was not found.");
return BadRequest(ModelState);
}
var templateRevisionId = templateSelection.TemplateRevisionId
?? templateVersion.TemplateRevisions.FirstOrDefault(revision => revision.IsCurrent)?.Id;
if (templateRevisionId.HasValue
&& !templateVersion.TemplateRevisions.Any(revision => revision.Id == templateRevisionId.Value))
{
ModelState.AddModelError(nameof(templateSelection.TemplateRevisionId), "Template revision was not found in this template version.");
return BadRequest(ModelState);
}
var sortOrder = templateSelection.SortOrder > 0
? templateSelection.SortOrder
: GetNextTemplateSelectionSortOrder(id);
if (_context.DeploymentTemplateSelections.Any(selection => selection.DeploymentGroupId == id && selection.SortOrder == sortOrder))
{
ModelState.AddModelError(nameof(templateSelection.SortOrder), "SortOrder already exists in this deployment batch.");
return BadRequest(ModelState);
}
var model = _mapper.Map<DeploymentTemplateSelectionModel>(templateSelection);
model.Id = Guid.NewGuid();
model.DeploymentGroupId = id;
model.SortOrder = sortOrder;
model.TemplateRevisionId = templateRevisionId;
_context.DeploymentTemplateSelections.Add(model);
_context.SaveChanges();
return Ok(model.Id);
}
[HttpDelete("{id}/template-selections/{selectionId}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteTemplateSelection(Guid id, Guid selectionId)
{
var selection = _context.DeploymentTemplateSelections
.FirstOrDefault(existing => existing.DeploymentGroupId == id && existing.Id == selectionId);
if (selection == null)
return NotFound();
_context.DeploymentTemplateSelections.Remove(selection);
_context.SaveChanges();
return NoContent();
}
[HttpPost("{id}/parameter-values")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public IActionResult AddParameterValue(Guid id, [FromBody] AddDeploymentParameterValueDto parameterValue)
{
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
return NotFound();
if (parameterValue.DeploymentTemplateSelectionId.HasValue
&& !_context.DeploymentTemplateSelections.Any(selection =>
selection.DeploymentGroupId == id
&& selection.Id == parameterValue.DeploymentTemplateSelectionId.Value))
{
ModelState.AddModelError(nameof(parameterValue.DeploymentTemplateSelectionId), "Template selection was not found in this deployment batch.");
return BadRequest(ModelState);
}
if (_context.DeploymentParameterValues.Any(existing =>
existing.DeploymentGroupId == id
&& existing.DeploymentTemplateSelectionId == parameterValue.DeploymentTemplateSelectionId
&& existing.Name == parameterValue.Name))
{
ModelState.AddModelError(nameof(parameterValue.Name), "Parameter value already exists in this scope.");
return BadRequest(ModelState);
}
try
{
parameterValue.ValueJson = ConfigurationDocumentValidator.NormalizeAndValidate(
parameterValue.ValueJson,
ConfigurationDocumentKind.Metadata).JsonData;
}
catch (ConfigurationDocumentValidationException ex)
{
ModelState.AddModelError(nameof(parameterValue.ValueJson), ex.Message);
return BadRequest(ModelState);
}
var model = _mapper.Map<DeploymentParameterValueModel>(parameterValue);
model.Id = Guid.NewGuid();
model.DeploymentGroupId = id;
_context.DeploymentParameterValues.Add(model);
_context.SaveChanges();
return Ok(model.Id);
}
[HttpDelete("{id}/parameter-values/{parameterValueId}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteParameterValue(Guid id, Guid parameterValueId)
{
var parameterValue = _context.DeploymentParameterValues
.FirstOrDefault(existing => existing.DeploymentGroupId == id && existing.Id == parameterValueId);
if (parameterValue == null)
return NotFound();
_context.DeploymentParameterValues.Remove(parameterValue);
_context.SaveChanges();
return NoContent();
}
[HttpPost("{id}/target-assignments")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(200, Type = typeof(Guid))]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public IActionResult AddTargetAssignment(Guid id, [FromBody] AddDeploymentTargetAssignmentDto targetAssignment)
{
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
return NotFound();
if (!_context.Targets.Any(target => target.Id == targetAssignment.TargetId))
{
ModelState.AddModelError(nameof(targetAssignment.TargetId), "Target target was not found.");
return BadRequest(ModelState);
}
if (_context.DeploymentTargetAssignments.Any(existing =>
existing.DeploymentGroupId == id
&& existing.TargetId == targetAssignment.TargetId
&& existing.RoleKey == targetAssignment.RoleKey))
{
ModelState.AddModelError(nameof(targetAssignment.TargetId), "Target assignment already exists for this role.");
return BadRequest(ModelState);
}
try
{
targetAssignment.NodeDataJson = ConfigurationDocumentValidator.NormalizeAndValidate(
targetAssignment.NodeDataJson,
ConfigurationDocumentKind.Metadata).JsonData;
}
catch (ConfigurationDocumentValidationException ex)
{
ModelState.AddModelError(nameof(targetAssignment.NodeDataJson), ex.Message);
return BadRequest(ModelState);
}
var sortOrder = targetAssignment.SortOrder > 0
? targetAssignment.SortOrder
: GetNextTargetAssignmentSortOrder(id);
var model = _mapper.Map<DeploymentTargetAssignmentModel>(targetAssignment);
model.Id = Guid.NewGuid();
model.DeploymentGroupId = id;
model.SortOrder = sortOrder;
_context.DeploymentTargetAssignments.Add(model);
_context.SaveChanges();
return Ok(model.Id);
}
[HttpDelete("{id}/target-assignments/{targetAssignmentId}")]
[Authorize(Policy = ApiPolicies.DeploymentWrite)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteTargetAssignment(Guid id, Guid targetAssignmentId)
{
var targetAssignment = _context.DeploymentTargetAssignments
.FirstOrDefault(existing => existing.DeploymentGroupId == id && existing.Id == targetAssignmentId);
if (targetAssignment == null)
return NotFound();
_context.DeploymentTargetAssignments.Remove(targetAssignment);
_context.SaveChanges();
return NoContent();
}
private GetDeploymentCompositionDto LoadComposition(Guid deploymentBatchId)
{
return new GetDeploymentCompositionDto
{
DeploymentBatchId = deploymentBatchId,
TemplateSelections = _mapper.Map<List<GetDeploymentTemplateSelectionDto>>(
_context.DeploymentTemplateSelections
.AsNoTracking()
.Include(selection => selection.TemplateVersion)
.Where(selection => selection.DeploymentGroupId == deploymentBatchId)
.OrderBy(selection => selection.SortOrder)
.ToList()),
ParameterValues = _mapper.Map<List<GetDeploymentParameterValueDto>>(
_context.DeploymentParameterValues
.AsNoTracking()
.Where(parameterValue => parameterValue.DeploymentGroupId == deploymentBatchId)
.OrderBy(parameterValue => parameterValue.Name)
.ToList()),
TargetAssignments = _mapper.Map<List<GetDeploymentTargetAssignmentDto>>(
_context.DeploymentTargetAssignments
.AsNoTracking()
.Include(target => target.Target)
.Where(target => target.DeploymentGroupId == deploymentBatchId)
.OrderBy(target => target.SortOrder)
.ToList())
};
}
private int GetNextTemplateSelectionSortOrder(Guid deploymentBatchId)
{
var maxSortOrder = _context.DeploymentTemplateSelections
.Where(selection => selection.DeploymentGroupId == deploymentBatchId)
.Select(selection => (int?)selection.SortOrder)
.Max() ?? 0;
return maxSortOrder + 10;
}
private int GetNextTargetAssignmentSortOrder(Guid deploymentBatchId)
{
var maxSortOrder = _context.DeploymentTargetAssignments
.Where(target => target.DeploymentGroupId == deploymentBatchId)
.Select(target => (int?)target.SortOrder)
.Max() ?? 0;
return maxSortOrder + 10;
}
}
}