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:
221
Controllers/ApiTokenController.cs
Normal file
221
Controllers/ApiTokenController.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.SelfService.Portal.Core.API.Context;
|
||||
using Microsoft.SelfService.Portal.Core.API.Dto.Auth;
|
||||
using Microsoft.SelfService.Portal.Core.API.Models;
|
||||
using Microsoft.SelfService.Portal.Core.API.Services;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
{
|
||||
[Route("api/tokens")]
|
||||
[ApiController]
|
||||
public class ApiTokenController : Controller
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly ApiTokenService _tokenService;
|
||||
|
||||
public ApiTokenController(DataContext context, ApiTokenService tokenService)
|
||||
{
|
||||
_context = context;
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
[HttpGet("my")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetManagedTokenDto>))]
|
||||
public IActionResult GetMyTokens()
|
||||
{
|
||||
var subject = GetCurrentSubject();
|
||||
var subjectType = GetCurrentSubjectType();
|
||||
|
||||
var tokens = _context.ApiTokens
|
||||
.Include(token => token.ApiClient)
|
||||
.Where(token => token.Subject == subject && token.SubjectType == subjectType)
|
||||
.OrderByDescending(token => token.IssuedAt)
|
||||
.ToList();
|
||||
|
||||
return Ok(tokens.Select(ToDto));
|
||||
}
|
||||
|
||||
[HttpPost("my")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(CreateManagedTokenResponseDto))]
|
||||
[ProducesResponseType(400)]
|
||||
public IActionResult CreateMyToken([FromBody] CreateManagedTokenRequestDto request)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Scope))
|
||||
{
|
||||
return BadRequest(new { message = "Scope is required." });
|
||||
}
|
||||
|
||||
IReadOnlyCollection<string> scopes;
|
||||
try
|
||||
{
|
||||
scopes = _tokenService.ResolveRequestedUserScopes(request.Scope);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { message = ex.Message });
|
||||
}
|
||||
|
||||
var subject = GetCurrentSubject();
|
||||
var subjectType = GetCurrentSubjectType();
|
||||
var name = string.IsNullOrWhiteSpace(request.Name)
|
||||
? $"Token for {subject}"
|
||||
: request.Name.Trim();
|
||||
|
||||
var token = _tokenService.CreateAccessToken(
|
||||
subject,
|
||||
subjectType == "ApiClient" ? subject : null,
|
||||
scopes);
|
||||
var now = DateTime.UtcNow;
|
||||
var expiresAt = request.ExpiresAt.HasValue && request.ExpiresAt.Value < token.ExpiresAt
|
||||
? request.ExpiresAt.Value
|
||||
: token.ExpiresAt;
|
||||
|
||||
if (expiresAt <= now)
|
||||
{
|
||||
return BadRequest(new { message = "ExpiresAt must be in the future." });
|
||||
}
|
||||
|
||||
var model = new ApiTokenModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Jti = token.Jti,
|
||||
Subject = subject,
|
||||
SubjectType = subjectType,
|
||||
Name = name,
|
||||
ScopesJson = JsonSerializer.Serialize(scopes),
|
||||
IssuedAt = token.IssuedAt,
|
||||
ExpiresAt = expiresAt,
|
||||
Created = now,
|
||||
CreatedBy = subject,
|
||||
Modified = now,
|
||||
ModifiedBy = subject
|
||||
};
|
||||
|
||||
_context.ApiTokens.Add(model);
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(new CreateManagedTokenResponseDto
|
||||
{
|
||||
Id = model.Id,
|
||||
AccessToken = token.AccessToken,
|
||||
ExpiresIn = Math.Max(0, Convert.ToInt32((expiresAt - now).TotalSeconds)),
|
||||
ExpiresAt = expiresAt,
|
||||
Scope = string.Join(' ', scopes)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("my/{id}")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(GetManagedTokenDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult RevokeMyToken(Guid id)
|
||||
{
|
||||
var subject = GetCurrentSubject();
|
||||
var subjectType = GetCurrentSubjectType();
|
||||
var token = _context.ApiTokens.FirstOrDefault(existing =>
|
||||
existing.Id == id
|
||||
&& existing.Subject == subject
|
||||
&& existing.SubjectType == subjectType);
|
||||
|
||||
if (token == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
RevokeToken(token, subject);
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(ToDto(token));
|
||||
}
|
||||
|
||||
[HttpGet("admin")]
|
||||
[Authorize(Policy = "TokenAdmin")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetManagedTokenDto>))]
|
||||
public IActionResult GetAllTokens()
|
||||
{
|
||||
var tokens = _context.ApiTokens
|
||||
.Include(token => token.ApiClient)
|
||||
.OrderByDescending(token => token.IssuedAt)
|
||||
.ToList();
|
||||
|
||||
return Ok(tokens.Select(ToDto));
|
||||
}
|
||||
|
||||
[HttpDelete("admin/{id}")]
|
||||
[Authorize(Policy = "TokenAdmin")]
|
||||
[ProducesResponseType(200, Type = typeof(GetManagedTokenDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult RevokeTokenAsAdmin(Guid id)
|
||||
{
|
||||
var token = _context.ApiTokens.FirstOrDefault(existing => existing.Id == id);
|
||||
if (token == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
RevokeToken(token, GetCurrentSubject());
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(ToDto(token));
|
||||
}
|
||||
|
||||
[HttpGet("scopes")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<string>))]
|
||||
public IActionResult GetAllowedScopes()
|
||||
{
|
||||
return Ok(_tokenService.ReadAllowedTokenScopes());
|
||||
}
|
||||
|
||||
private string GetCurrentSubject()
|
||||
{
|
||||
return User.FindFirstValue("client_id")
|
||||
?? User.FindFirstValue(ClaimTypes.Name)
|
||||
?? User.Identity?.Name
|
||||
?? "Unknown";
|
||||
}
|
||||
|
||||
private string GetCurrentSubjectType()
|
||||
{
|
||||
return User.HasClaim(claim => claim.Type == "client_id") ? "ApiClient" : "User";
|
||||
}
|
||||
|
||||
private static void RevokeToken(ApiTokenModel token, string revokedBy)
|
||||
{
|
||||
if (!token.RevokedAt.HasValue)
|
||||
{
|
||||
token.RevokedAt = DateTime.UtcNow;
|
||||
token.RevokedBy = revokedBy;
|
||||
token.Modified = DateTime.UtcNow;
|
||||
token.ModifiedBy = revokedBy;
|
||||
}
|
||||
}
|
||||
|
||||
private static GetManagedTokenDto ToDto(ApiTokenModel token)
|
||||
{
|
||||
var scopes = JsonSerializer.Deserialize<string[]>(token.ScopesJson) ?? Array.Empty<string>();
|
||||
|
||||
return new GetManagedTokenDto
|
||||
{
|
||||
Id = token.Id,
|
||||
Subject = token.Subject,
|
||||
SubjectType = token.SubjectType,
|
||||
ClientId = token.ApiClient?.ClientId,
|
||||
Name = token.Name,
|
||||
Scope = string.Join(' ', scopes),
|
||||
IssuedAt = token.IssuedAt,
|
||||
ExpiresAt = token.ExpiresAt,
|
||||
RevokedAt = token.RevokedAt,
|
||||
RevokedBy = token.RevokedBy,
|
||||
LastUsedAt = token.LastUsedAt,
|
||||
IsActive = !token.RevokedAt.HasValue && token.ExpiresAt > DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Controllers/AuthController.cs
Normal file
94
Controllers/AuthController.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.SelfService.Portal.Core.API.Context;
|
||||
using Microsoft.SelfService.Portal.Core.API.Dto.Auth;
|
||||
using Microsoft.SelfService.Portal.Core.API.Models;
|
||||
using Microsoft.SelfService.Portal.Core.API.Services;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
{
|
||||
[Route("api/auth")]
|
||||
[ApiController]
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly ApiTokenService _tokenService;
|
||||
|
||||
public AuthController(DataContext context, ApiTokenService tokenService)
|
||||
{
|
||||
_context = context;
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
[HttpPost("token")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(200, Type = typeof(TokenResponseDto))]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(401)]
|
||||
public IActionResult CreateToken([FromBody] TokenRequestDto request)
|
||||
{
|
||||
if (request == null
|
||||
|| string.IsNullOrWhiteSpace(request.ClientId)
|
||||
|| string.IsNullOrWhiteSpace(request.ClientSecret))
|
||||
{
|
||||
return BadRequest(new { message = "ClientId and ClientSecret are required." });
|
||||
}
|
||||
|
||||
var client = _context.ApiClients
|
||||
.FirstOrDefault(existing => existing.ClientId == request.ClientId);
|
||||
|
||||
if (client == null
|
||||
|| !client.IsEnabled
|
||||
|| (client.ExpiresAt.HasValue && client.ExpiresAt.Value <= DateTime.UtcNow)
|
||||
|| !ApiClientSecretHasher.VerifySecret(request.ClientSecret, client.SecretHash))
|
||||
{
|
||||
return Unauthorized(new { message = "Invalid client credentials." });
|
||||
}
|
||||
|
||||
IReadOnlyCollection<string> scopes;
|
||||
try
|
||||
{
|
||||
scopes = _tokenService.ResolveRequestedScopes(client, request.Scope);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Unauthorized(new { message = ex.Message });
|
||||
}
|
||||
|
||||
var token = _tokenService.CreateAccessToken(client, scopes);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
client.LastUsedAt = now;
|
||||
client.Modified = now;
|
||||
client.ModifiedBy = client.ClientId;
|
||||
|
||||
_context.ApiTokens.Add(new ApiTokenModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Jti = token.Jti,
|
||||
Subject = client.ClientId,
|
||||
SubjectType = "ApiClient",
|
||||
ApiClientId = client.Id,
|
||||
Name = $"Client credentials token for {client.Name}",
|
||||
ScopesJson = JsonSerializer.Serialize(scopes),
|
||||
IssuedAt = token.IssuedAt,
|
||||
ExpiresAt = token.ExpiresAt,
|
||||
Created = now,
|
||||
CreatedBy = client.ClientId,
|
||||
Modified = now,
|
||||
ModifiedBy = client.ClientId
|
||||
});
|
||||
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(new TokenResponseDto
|
||||
{
|
||||
AccessToken = token.AccessToken,
|
||||
ExpiresIn = _tokenService.TokenLifetimeSeconds,
|
||||
Scope = string.Join(' ', scopes)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Controllers/ConfigurationValueController.cs
Normal file
106
Controllers/ConfigurationValueController.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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;
|
||||
using Microsoft.SelfService.Portal.Core.API.JsonDocuments;
|
||||
using Microsoft.SelfService.Portal.Core.API.Models;
|
||||
|
||||
namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
{
|
||||
[Route("api/configuration-values")]
|
||||
[ApiController]
|
||||
public class ConfigurationValueController : Controller
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ConfigurationValueController(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetConfigurationValueDto>))]
|
||||
public IActionResult GetConfigurationValues([FromQuery] Guid? definitionId, [FromQuery] string? scopeType, [FromQuery] Guid? scopeId)
|
||||
{
|
||||
var query = _context.ConfigurationValues
|
||||
.AsNoTracking()
|
||||
.Include(value => value.ConfigurationDefinition)
|
||||
.AsQueryable();
|
||||
|
||||
if (definitionId.HasValue)
|
||||
query = query.Where(value => value.ConfigurationDefinitionId == definitionId.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(scopeType))
|
||||
query = query.Where(value => value.ScopeType == scopeType);
|
||||
|
||||
if (scopeId.HasValue)
|
||||
query = query.Where(value => value.ScopeId == scopeId.Value);
|
||||
|
||||
var values = query
|
||||
.OrderBy(value => value.ScopeType)
|
||||
.ThenBy(value => value.SortOrder)
|
||||
.ThenBy(value => value.ConfigurationDefinition.Name)
|
||||
.ToList();
|
||||
|
||||
return Ok(_mapper.Map<List<GetConfigurationValueDto>>(values));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(200, Type = typeof(Guid))]
|
||||
[ProducesResponseType(400)]
|
||||
public IActionResult AddConfigurationValue([FromBody] AddConfigurationValueDto configurationValue)
|
||||
{
|
||||
if (configurationValue == null)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
if (!_context.ConfigurationDefinitions.Any(definition => definition.Id == configurationValue.ConfigurationDefinitionId))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configurationValue.ConfigurationDefinitionId), "Configuration definition was not found.");
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(configurationValue.ValueJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
configurationValue.ValueJson = ConfigurationDocumentValidator.NormalizeAndValidate(
|
||||
configurationValue.ValueJson,
|
||||
ConfigurationDocumentKind.Metadata).JsonData;
|
||||
}
|
||||
catch (ConfigurationDocumentValidationException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(configurationValue.ValueJson), ex.Message);
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
var model = _mapper.Map<ConfigurationValueModel>(configurationValue);
|
||||
model.Id = Guid.NewGuid();
|
||||
model.ValueHash = TemplateVersionModel.ComputeSha256(model.ValueJson ?? model.SourcePath ?? string.Empty);
|
||||
|
||||
_context.ConfigurationValues.Add(model);
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(model.Id);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult DeleteConfigurationValue(Guid id)
|
||||
{
|
||||
var value = _context.ConfigurationValues.FirstOrDefault(existing => existing.Id == id);
|
||||
if (value == null)
|
||||
return NotFound();
|
||||
|
||||
_context.ConfigurationValues.Remove(value);
|
||||
_context.SaveChanges();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
143
Controllers/CredentialSecretController.cs
Normal file
143
Controllers/CredentialSecretController.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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;
|
||||
using Microsoft.SelfService.Portal.Core.API.JsonDocuments;
|
||||
using Microsoft.SelfService.Portal.Core.API.Models;
|
||||
|
||||
namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
{
|
||||
[Route("api/credential-secrets")]
|
||||
[ApiController]
|
||||
public class CredentialSecretController : Controller
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public CredentialSecretController(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetCredentialSecretDto>))]
|
||||
public IActionResult GetCredentialSecrets()
|
||||
{
|
||||
var secrets = _context.CredentialSecrets
|
||||
.AsNoTracking()
|
||||
.OrderBy(secret => secret.Name)
|
||||
.ToList();
|
||||
|
||||
return Ok(_mapper.Map<List<GetCredentialSecretDto>>(secrets));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(200, Type = typeof(GetCredentialSecretDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult GetCredentialSecretById(Guid id)
|
||||
{
|
||||
var secret = _context.CredentialSecrets
|
||||
.AsNoTracking()
|
||||
.FirstOrDefault(existing => existing.Id == id);
|
||||
|
||||
if (secret == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<GetCredentialSecretDto>(secret));
|
||||
}
|
||||
|
||||
[HttpGet("resolve")]
|
||||
[ProducesResponseType(200, Type = typeof(GetCredentialSecretValueDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult ResolveCredentialSecret([FromQuery] string name)
|
||||
{
|
||||
var secret = _context.CredentialSecrets
|
||||
.AsNoTracking()
|
||||
.FirstOrDefault(existing => existing.Name == name && existing.IsEnabled);
|
||||
|
||||
if (secret == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<GetCredentialSecretValueDto>(secret));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(200, Type = typeof(Guid))]
|
||||
[ProducesResponseType(400)]
|
||||
public IActionResult AddCredentialSecret([FromBody] AddCredentialSecretDto credentialSecret)
|
||||
{
|
||||
if (credentialSecret == null)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
if (_context.CredentialSecrets.Any(existing => existing.Name == credentialSecret.Name))
|
||||
{
|
||||
ModelState.AddModelError(nameof(credentialSecret.Name), "Credential secret already exists.");
|
||||
return StatusCode(422, ModelState);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(credentialSecret.MetadataJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
credentialSecret.MetadataJson = ConfigurationDocumentValidator.NormalizeAndValidate(
|
||||
credentialSecret.MetadataJson,
|
||||
ConfigurationDocumentKind.Metadata).JsonData;
|
||||
}
|
||||
catch (ConfigurationDocumentValidationException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(credentialSecret.MetadataJson), ex.Message);
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
var model = _mapper.Map<CredentialSecretModel>(credentialSecret);
|
||||
model.Id = Guid.NewGuid();
|
||||
|
||||
_context.CredentialSecrets.Add(model);
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(model.Id);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult UpdateCredentialSecret(Guid id, [FromBody] AddCredentialSecretDto credentialSecret)
|
||||
{
|
||||
var existing = _context.CredentialSecrets.FirstOrDefault(secret => secret.Id == id);
|
||||
if (existing == null)
|
||||
return NotFound();
|
||||
|
||||
existing.Name = credentialSecret.Name;
|
||||
existing.UserName = credentialSecret.UserName;
|
||||
existing.SecretValue = credentialSecret.SecretValue;
|
||||
existing.SecretType = credentialSecret.SecretType;
|
||||
existing.MetadataJson = credentialSecret.MetadataJson;
|
||||
existing.IsEnabled = credentialSecret.IsEnabled;
|
||||
existing.Modified = DateTime.UtcNow;
|
||||
existing.ModifiedBy = User?.Identity?.Name ?? "System";
|
||||
|
||||
_context.SaveChanges();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult DeleteCredentialSecret(Guid id)
|
||||
{
|
||||
var secret = _context.CredentialSecrets.FirstOrDefault(existing => existing.Id == id);
|
||||
if (secret == null)
|
||||
return NotFound();
|
||||
|
||||
_context.CredentialSecrets.Remove(secret);
|
||||
_context.SaveChanges();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,12 +190,26 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
if (!_deploymentBatchInterface.CheckDeploymentBatchById(id))
|
||||
return NotFound();
|
||||
|
||||
if (!_context.TemplateVersions.Any(version => version.Id == templateSelection.TemplateVersionId))
|
||||
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);
|
||||
@@ -210,6 +224,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
model.Id = Guid.NewGuid();
|
||||
model.DeploymentGroupId = id;
|
||||
model.SortOrder = sortOrder;
|
||||
model.TemplateRevisionId = templateRevisionId;
|
||||
|
||||
_context.DeploymentTemplateSelections.Add(model);
|
||||
_context.SaveChanges();
|
||||
|
||||
@@ -108,7 +108,8 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
var queueJobId = _queueJobService.EnqueueDeploymentRequest(
|
||||
request.DeploymentGroupId,
|
||||
request.TargetIds,
|
||||
request.JsonData);
|
||||
request.JsonData,
|
||||
request.DeploymentArtifactId);
|
||||
|
||||
return Ok(queueJobId);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ using Microsoft.AspNetCore.Mvc;
|
||||
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;
|
||||
using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationDefinition.Get;
|
||||
using Microsoft.SelfService.Portal.Core.API.Dto.TemplateRevision.Add;
|
||||
using Microsoft.SelfService.Portal.Core.API.Dto.TemplateRevision.Get;
|
||||
using Microsoft.SelfService.Portal.Core.API.Dto.TemplateVersion.Add;
|
||||
using Microsoft.SelfService.Portal.Core.API.Dto.TemplateVersion.Get;
|
||||
using Microsoft.SelfService.Portal.Core.API.Interfaces;
|
||||
@@ -223,6 +226,77 @@ namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
return Ok(version.Id);
|
||||
}
|
||||
|
||||
[HttpGet("{Id}/Versions/{VersionId}/Revisions")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetTemplateRevisionDto>))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult GetTemplateRevisions(Guid Id, Guid VersionId)
|
||||
{
|
||||
if (!_templateInterface.CheckTemplateVersionById(Id, VersionId))
|
||||
return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<List<GetTemplateRevisionDto>>(_templateInterface.GetTemplateRevisions(Id, VersionId)));
|
||||
}
|
||||
|
||||
[HttpGet("{Id}/Versions/{VersionId}/Revisions/{RevisionId}")]
|
||||
[ProducesResponseType(200, Type = typeof(GetTemplateRevisionDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult GetTemplateRevisionById(Guid Id, Guid VersionId, Guid RevisionId)
|
||||
{
|
||||
var revision = _templateInterface.GetTemplateRevisionById(Id, VersionId, RevisionId);
|
||||
if (revision == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<GetTemplateRevisionDto>(revision));
|
||||
}
|
||||
|
||||
[HttpGet("{Id}/Versions/{VersionId}/Revisions/{RevisionId}/Definitions")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetConfigurationDefinitionDto>))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult GetTemplateRevisionDefinitions(Guid Id, Guid VersionId, Guid RevisionId)
|
||||
{
|
||||
var revision = _templateInterface.GetTemplateRevisionById(Id, VersionId, RevisionId);
|
||||
if (revision == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<List<GetConfigurationDefinitionDto>>(revision.ConfigurationDefinitions));
|
||||
}
|
||||
|
||||
[HttpPost("{Id}/Versions/{VersionId}/Revisions")]
|
||||
[ProducesResponseType(200, Type = typeof(Guid))]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult AddTemplateRevision(Guid Id, Guid VersionId, [FromBody] AddTemplateRevisionDto templateRevision)
|
||||
{
|
||||
if (templateRevision == null)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
if (!_templateInterface.CheckTemplateVersionById(Id, VersionId))
|
||||
return NotFound();
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
var revision = _mapper.Map<TemplateRevisionModel>(templateRevision);
|
||||
revision.PublishedBy = templateRevision.PublishedBy ?? User?.Identity?.Name ?? "System";
|
||||
|
||||
try
|
||||
{
|
||||
if (!_templateInterface.AddTemplateRevision(Id, VersionId, revision, templateRevision.Publish))
|
||||
{
|
||||
ModelState.AddModelError("", "Something went wrong while saving template revision");
|
||||
return StatusCode(500, ModelState);
|
||||
}
|
||||
}
|
||||
catch (ConfigurationDocumentValidationException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(templateRevision.JsonData), ex.Message);
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
var savedRevision = _templateInterface.GetCurrentTemplateRevision(VersionId);
|
||||
return Ok(savedRevision?.Id ?? revision.Id);
|
||||
}
|
||||
|
||||
[HttpPost("{Id}/Versions/{VersionId}/Publish")]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(404)]
|
||||
|
||||
Reference in New Issue
Block a user