- 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.
222 lines
7.5 KiB
C#
222 lines
7.5 KiB
C#
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
|
|
};
|
|
}
|
|
}
|
|
}
|