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:
266
Controllers/ConfigurationDefinitionController.cs
Normal file
266
Controllers/ConfigurationDefinitionController.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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]
|
||||
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]
|
||||
[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}")]
|
||||
[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}")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user