Files
git.stella-ops.org/src/Cli/StellaOps.Cli/Extensions/StellaOpsTokenClientExtensions.cs

129 lines
5.0 KiB
C#

using StellaOps.Auth.Client;
using StellaOps.Cli.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Extensions;
/// <summary>
/// Extension methods for IStellaOpsTokenClient providing compatibility with older CLI patterns.
/// These bridge the gap between the old API (GetTokenAsync, GetAccessTokenAsync) and the
/// new API (RequestClientCredentialsTokenAsync, GetCachedTokenAsync).
/// </summary>
public static class StellaOpsTokenClientExtensions
{
/// <summary>
/// Requests an access token using client credentials flow with the specified scopes.
/// This is a compatibility shim for the old GetAccessTokenAsync pattern.
/// </summary>
public static async Task<StellaOpsTokenResult> GetAccessTokenAsync(
this IStellaOpsTokenClient client,
IEnumerable<string> scopes,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(client);
var scope = scopes is not null ? string.Join(" ", scopes.Where(s => !string.IsNullOrWhiteSpace(s))) : null;
return await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Requests an access token using client credentials flow with a single scope.
/// </summary>
public static async Task<StellaOpsTokenResult> GetAccessTokenAsync(
this IStellaOpsTokenClient client,
string scope,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(client);
return await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Gets a cached access token or requests a new one if not cached or expired.
/// This is a compatibility shim for the old GetCachedAccessTokenAsync pattern.
/// Cache key includes effective tenant to prevent cross-tenant cache collisions.
/// </summary>
public static async Task<StellaOpsTokenCacheEntry> GetCachedAccessTokenAsync(
this IStellaOpsTokenClient client,
IEnumerable<string> scopes,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(client);
var scopeList = scopes?.Where(s => !string.IsNullOrWhiteSpace(s)).OrderBy(s => s).ToArray() ?? [];
var scope = string.Join(" ", scopeList);
var cacheKey = BuildCacheKey(scope);
// Check cache first
var cached = await client.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
if (cached is not null && !cached.IsExpired(TimeProvider.System, TimeSpan.FromMinutes(1)))
{
return cached;
}
// Request new token
var result = await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
var entry = result.ToCacheEntry();
// Cache the result
await client.CacheTokenAsync(cacheKey, entry, cancellationToken).ConfigureAwait(false);
return entry;
}
/// <summary>
/// Gets a cached access token or requests a new one if not cached or expired.
/// Single scope version.
/// </summary>
public static async Task<StellaOpsTokenCacheEntry> GetCachedAccessTokenAsync(
this IStellaOpsTokenClient client,
string scope,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(client);
var cacheKey = BuildCacheKey(scope ?? "default");
// Check cache first
var cached = await client.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
if (cached is not null && !cached.IsExpired(TimeProvider.System, TimeSpan.FromMinutes(1)))
{
return cached;
}
// Request new token
var result = await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
var entry = result.ToCacheEntry();
// Cache the result
await client.CacheTokenAsync(cacheKey, entry, cancellationToken).ConfigureAwait(false);
return entry;
}
/// <summary>
/// Requests a token using client credentials. Parameterless version for simple cases.
/// </summary>
public static async Task<StellaOpsTokenResult> GetTokenAsync(
this IStellaOpsTokenClient client,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(client);
return await client.RequestClientCredentialsTokenAsync(null, null, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Builds a cache key that includes the effective tenant to prevent cross-tenant
/// token cache collisions when users switch tenants between CLI invocations.
/// </summary>
private static string BuildCacheKey(string scope)
{
var tenant = TenantProfileStore.GetEffectiveTenant(null) ?? "none";
return $"cc:{tenant}:{scope}";
}
}