up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,129 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Registry.TokenService.Observability;
|
||||
using StellaOps.Registry.TokenService.Security;
|
||||
|
||||
namespace StellaOps.Registry.TokenService;
|
||||
|
||||
public sealed class RegistryTokenIssuer
|
||||
{
|
||||
private readonly RegistryTokenServiceOptions _options;
|
||||
private readonly PlanRegistry _planRegistry;
|
||||
private readonly RegistryTokenMetrics _metrics;
|
||||
private readonly SigningCredentials _signingCredentials;
|
||||
private readonly JwtSecurityTokenHandler _tokenHandler = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RegistryTokenIssuer(
|
||||
IOptions<RegistryTokenServiceOptions> options,
|
||||
PlanRegistry planRegistry,
|
||||
RegistryTokenMetrics metrics,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(planRegistry);
|
||||
ArgumentNullException.ThrowIfNull(metrics);
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
|
||||
_options = options.Value;
|
||||
_planRegistry = planRegistry;
|
||||
_metrics = metrics;
|
||||
_timeProvider = timeProvider;
|
||||
_signingCredentials = SigningKeyLoader.Load(_options.Signing);
|
||||
}
|
||||
|
||||
public RegistryTokenResponse IssueToken(
|
||||
ClaimsPrincipal principal,
|
||||
string service,
|
||||
IReadOnlyList<RegistryAccessRequest> requests)
|
||||
{
|
||||
var decision = _planRegistry.Authorize(principal, requests);
|
||||
if (!decision.Allowed)
|
||||
{
|
||||
_metrics.TokensRejected.Add(1, new KeyValuePair<string, object?>("reason", decision.FailureReason ?? "denied"));
|
||||
throw new RegistryTokenException(decision.FailureReason ?? "denied");
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var expires = now + _options.Signing.Lifetime;
|
||||
var subject = principal.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? principal.FindFirstValue("client_id")
|
||||
?? principal.FindFirstValue("sub")
|
||||
?? "anonymous";
|
||||
|
||||
var payload = new JwtPayload(
|
||||
issuer: _options.Signing.Issuer,
|
||||
audience: _options.Signing.Audience ?? service,
|
||||
claims: null,
|
||||
notBefore: now.UtcDateTime,
|
||||
expires: expires.UtcDateTime,
|
||||
issuedAt: now.UtcDateTime)
|
||||
{
|
||||
{ JwtRegisteredClaimNames.Sub, subject },
|
||||
{ JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("n") },
|
||||
{ "service", service },
|
||||
{ "access", BuildAccessClaim(requests) }
|
||||
};
|
||||
|
||||
var licenseId = principal.FindFirstValue("stellaops:license");
|
||||
if (!string.IsNullOrWhiteSpace(licenseId))
|
||||
{
|
||||
payload["stellaops:license"] = licenseId;
|
||||
}
|
||||
|
||||
var token = new JwtSecurityToken(new JwtHeader(_signingCredentials), payload);
|
||||
var serialized = _tokenHandler.WriteToken(token);
|
||||
|
||||
var plan = principal.FindFirstValue("stellaops:plan") ?? _options.DefaultPlan ?? "unknown";
|
||||
_metrics.TokensIssued.Add(1, new KeyValuePair<string, object?>("plan", plan));
|
||||
|
||||
return new RegistryTokenResponse(
|
||||
serialized,
|
||||
(int)_options.Signing.Lifetime.TotalSeconds,
|
||||
now);
|
||||
}
|
||||
|
||||
private static object BuildAccessClaim(IReadOnlyList<RegistryAccessRequest> requests)
|
||||
{
|
||||
return requests
|
||||
.Select(request => new Dictionary<string, object>
|
||||
{
|
||||
["type"] = request.Type,
|
||||
["name"] = request.Name,
|
||||
["actions"] = request.Actions
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RegistryTokenResponse
|
||||
{
|
||||
public RegistryTokenResponse(string token, int expiresInSeconds, DateTimeOffset issuedAt)
|
||||
{
|
||||
Token = token;
|
||||
ExpiresIn = expiresInSeconds;
|
||||
IssuedAt = issuedAt;
|
||||
}
|
||||
|
||||
public string Token { get; }
|
||||
|
||||
public int ExpiresIn { get; }
|
||||
|
||||
public DateTimeOffset IssuedAt { get; }
|
||||
}
|
||||
|
||||
public sealed class RegistryTokenException : Exception
|
||||
{
|
||||
public RegistryTokenException(string reason)
|
||||
: base($"Token request denied: {reason}")
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public string Reason { get; }
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Registry.TokenService.Observability;
|
||||
using StellaOps.Registry.TokenService.Security;
|
||||
|
||||
namespace StellaOps.Registry.TokenService;
|
||||
|
||||
public sealed class RegistryTokenIssuer
|
||||
{
|
||||
private readonly RegistryTokenServiceOptions _options;
|
||||
private readonly PlanRegistry _planRegistry;
|
||||
private readonly RegistryTokenMetrics _metrics;
|
||||
private readonly SigningCredentials _signingCredentials;
|
||||
private readonly JwtSecurityTokenHandler _tokenHandler = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RegistryTokenIssuer(
|
||||
IOptions<RegistryTokenServiceOptions> options,
|
||||
PlanRegistry planRegistry,
|
||||
RegistryTokenMetrics metrics,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(planRegistry);
|
||||
ArgumentNullException.ThrowIfNull(metrics);
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
|
||||
_options = options.Value;
|
||||
_planRegistry = planRegistry;
|
||||
_metrics = metrics;
|
||||
_timeProvider = timeProvider;
|
||||
_signingCredentials = SigningKeyLoader.Load(_options.Signing);
|
||||
}
|
||||
|
||||
public RegistryTokenResponse IssueToken(
|
||||
ClaimsPrincipal principal,
|
||||
string service,
|
||||
IReadOnlyList<RegistryAccessRequest> requests)
|
||||
{
|
||||
var decision = _planRegistry.Authorize(principal, requests);
|
||||
if (!decision.Allowed)
|
||||
{
|
||||
_metrics.TokensRejected.Add(1, new KeyValuePair<string, object?>("reason", decision.FailureReason ?? "denied"));
|
||||
throw new RegistryTokenException(decision.FailureReason ?? "denied");
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var expires = now + _options.Signing.Lifetime;
|
||||
var subject = principal.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? principal.FindFirstValue("client_id")
|
||||
?? principal.FindFirstValue("sub")
|
||||
?? "anonymous";
|
||||
|
||||
var payload = new JwtPayload(
|
||||
issuer: _options.Signing.Issuer,
|
||||
audience: _options.Signing.Audience ?? service,
|
||||
claims: null,
|
||||
notBefore: now.UtcDateTime,
|
||||
expires: expires.UtcDateTime,
|
||||
issuedAt: now.UtcDateTime)
|
||||
{
|
||||
{ JwtRegisteredClaimNames.Sub, subject },
|
||||
{ JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("n") },
|
||||
{ "service", service },
|
||||
{ "access", BuildAccessClaim(requests) }
|
||||
};
|
||||
|
||||
var licenseId = principal.FindFirstValue("stellaops:license");
|
||||
if (!string.IsNullOrWhiteSpace(licenseId))
|
||||
{
|
||||
payload["stellaops:license"] = licenseId;
|
||||
}
|
||||
|
||||
var token = new JwtSecurityToken(new JwtHeader(_signingCredentials), payload);
|
||||
var serialized = _tokenHandler.WriteToken(token);
|
||||
|
||||
var plan = principal.FindFirstValue("stellaops:plan") ?? _options.DefaultPlan ?? "unknown";
|
||||
_metrics.TokensIssued.Add(1, new KeyValuePair<string, object?>("plan", plan));
|
||||
|
||||
return new RegistryTokenResponse(
|
||||
serialized,
|
||||
(int)_options.Signing.Lifetime.TotalSeconds,
|
||||
now);
|
||||
}
|
||||
|
||||
private static object BuildAccessClaim(IReadOnlyList<RegistryAccessRequest> requests)
|
||||
{
|
||||
return requests
|
||||
.Select(request => new Dictionary<string, object>
|
||||
{
|
||||
["type"] = request.Type,
|
||||
["name"] = request.Name,
|
||||
["actions"] = request.Actions
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RegistryTokenResponse
|
||||
{
|
||||
public RegistryTokenResponse(string token, int expiresInSeconds, DateTimeOffset issuedAt)
|
||||
{
|
||||
Token = token;
|
||||
ExpiresIn = expiresInSeconds;
|
||||
IssuedAt = issuedAt;
|
||||
}
|
||||
|
||||
public string Token { get; }
|
||||
|
||||
public int ExpiresIn { get; }
|
||||
|
||||
public DateTimeOffset IssuedAt { get; }
|
||||
}
|
||||
|
||||
public sealed class RegistryTokenException : Exception
|
||||
{
|
||||
public RegistryTokenException(string reason)
|
||||
: base($"Token request denied: {reason}")
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public string Reason { get; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user