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:
198
Controllers/DeploymentArtifactController.cs
Normal file
198
Controllers/DeploymentArtifactController.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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;
|
||||
using Microsoft.SelfService.Portal.Core.API.JsonDocuments;
|
||||
using Microsoft.SelfService.Portal.Core.API.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
{
|
||||
[Route("api/deployment-artifacts")]
|
||||
[ApiController]
|
||||
public class DeploymentArtifactController : Controller
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public DeploymentArtifactController(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetDeploymentArtifactDto>))]
|
||||
public IActionResult GetDeploymentArtifacts([FromQuery] Guid? deploymentGroupId, [FromQuery] Guid? targetId)
|
||||
{
|
||||
var query = _context.DeploymentArtifacts
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (deploymentGroupId.HasValue)
|
||||
query = query.Where(artifact => artifact.DeploymentGroupId == deploymentGroupId.Value);
|
||||
|
||||
if (targetId.HasValue)
|
||||
query = query.Where(artifact => artifact.TargetId == targetId.Value);
|
||||
|
||||
var artifacts = query
|
||||
.OrderByDescending(artifact => artifact.Created)
|
||||
.ToList();
|
||||
|
||||
return Ok(_mapper.Map<List<GetDeploymentArtifactDto>>(artifacts));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(200, Type = typeof(GetDeploymentArtifactDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult GetDeploymentArtifactById(Guid id)
|
||||
{
|
||||
var artifact = _context.DeploymentArtifacts
|
||||
.AsNoTracking()
|
||||
.FirstOrDefault(existing => existing.Id == id);
|
||||
|
||||
if (artifact == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<GetDeploymentArtifactDto>(artifact));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(200, Type = typeof(Guid))]
|
||||
[ProducesResponseType(400)]
|
||||
public IActionResult AddDeploymentArtifact([FromBody] AddDeploymentArtifactDto deploymentArtifact)
|
||||
{
|
||||
if (deploymentArtifact == null)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
if (!_context.DeploymentGroups.Any(group => group.Id == deploymentArtifact.DeploymentGroupId))
|
||||
{
|
||||
ModelState.AddModelError(nameof(deploymentArtifact.DeploymentGroupId), "Deployment group was not found.");
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
if (deploymentArtifact.TargetId.HasValue && !_context.Targets.Any(target => target.Id == deploymentArtifact.TargetId.Value))
|
||||
{
|
||||
ModelState.AddModelError(nameof(deploymentArtifact.TargetId), "Target was not found.");
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
deploymentArtifact.DeploymentJson = NormalizeJson(deploymentArtifact.DeploymentJson);
|
||||
deploymentArtifact.SourceJson = NormalizeOptionalJson(deploymentArtifact.SourceJson);
|
||||
deploymentArtifact.ResolvedJson = NormalizeOptionalJson(deploymentArtifact.ResolvedJson);
|
||||
deploymentArtifact.SourceSnapshotJson = NormalizeOptionalJson(deploymentArtifact.SourceSnapshotJson);
|
||||
}
|
||||
catch (ConfigurationDocumentValidationException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(deploymentArtifact.DeploymentJson), ex.Message);
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
var model = _mapper.Map<DeploymentArtifactModel>(deploymentArtifact);
|
||||
model.Id = Guid.NewGuid();
|
||||
model.InputHash = TemplateVersionModel.ComputeSha256(model.SourceJson ?? model.DeploymentJson);
|
||||
model.OutputHash = TemplateVersionModel.ComputeSha256(model.ResolvedJson ?? model.DeploymentJson);
|
||||
model.IsStale = false;
|
||||
|
||||
_context.DeploymentArtifacts.Add(model);
|
||||
|
||||
if (model.TargetId.HasValue)
|
||||
{
|
||||
var deployment = _context.Deployments.FirstOrDefault(existing =>
|
||||
existing.DeploymentGroupId == model.DeploymentGroupId
|
||||
&& existing.TargetId == model.TargetId.Value);
|
||||
|
||||
if (deployment != null)
|
||||
{
|
||||
deployment.CurrentArtifactId = model.Id;
|
||||
deployment.SourceJson = model.SourceJson;
|
||||
deployment.JSONData = model.DeploymentJson;
|
||||
}
|
||||
}
|
||||
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(model.Id);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/stale-check")]
|
||||
[ProducesResponseType(200, Type = typeof(GetDeploymentArtifactDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult CheckDeploymentArtifactStale(Guid id)
|
||||
{
|
||||
var artifact = _context.DeploymentArtifacts.FirstOrDefault(existing => existing.Id == id);
|
||||
if (artifact == null)
|
||||
return NotFound();
|
||||
|
||||
var staleReasons = BuildStaleReasons(artifact);
|
||||
artifact.IsStale = staleReasons.Count > 0;
|
||||
artifact.StaleReasonJson = staleReasons.Count > 0
|
||||
? JsonSerializer.Serialize(staleReasons)
|
||||
: null;
|
||||
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(_mapper.Map<GetDeploymentArtifactDto>(artifact));
|
||||
}
|
||||
|
||||
private static string NormalizeJson(string json)
|
||||
{
|
||||
return ConfigurationDocumentValidator.NormalizeAndValidate(
|
||||
string.IsNullOrWhiteSpace(json) ? "{}" : json,
|
||||
ConfigurationDocumentKind.Metadata).JsonData;
|
||||
}
|
||||
|
||||
private static string? NormalizeOptionalJson(string? json)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(json) ? null : NormalizeJson(json);
|
||||
}
|
||||
|
||||
private List<object> BuildStaleReasons(DeploymentArtifactModel artifact)
|
||||
{
|
||||
var reasons = new List<object>();
|
||||
if (string.IsNullOrWhiteSpace(artifact.SourceSnapshotJson))
|
||||
{
|
||||
return reasons;
|
||||
}
|
||||
|
||||
using var snapshot = JsonDocument.Parse(artifact.SourceSnapshotJson);
|
||||
if (!snapshot.RootElement.TryGetProperty("templateRevisions", out var revisions)
|
||||
|| revisions.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return reasons;
|
||||
}
|
||||
|
||||
foreach (var source in revisions.EnumerateArray())
|
||||
{
|
||||
if (!source.TryGetProperty("id", out var idElement)
|
||||
|| !Guid.TryParse(idElement.GetString(), out var revisionId)
|
||||
|| !source.TryGetProperty("hash", out var hashElement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var current = _context.TemplateRevisions
|
||||
.AsNoTracking()
|
||||
.FirstOrDefault(revision => revision.Id == revisionId);
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
reasons.Add(new { Type = "TemplateRevisionMissing", RevisionId = revisionId });
|
||||
continue;
|
||||
}
|
||||
|
||||
var expectedHash = hashElement.GetString();
|
||||
if (!string.Equals(current.JsonHash, expectedHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reasons.Add(new { Type = "TemplateRevisionHashChanged", RevisionId = revisionId, OldHash = expectedHash, CurrentHash = current.JsonHash });
|
||||
}
|
||||
}
|
||||
|
||||
return reasons;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user