up
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
41
src/Cli/StellaOps.Cli/Services/AuthorityConsoleClient.cs
Normal file
41
src/Cli/StellaOps.Cli/Services/AuthorityConsoleClient.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for Authority console endpoints (CLI-TEN-47-001).
|
||||
/// </summary>
|
||||
internal sealed class AuthorityConsoleClient : IAuthorityConsoleClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public AuthorityConsoleClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TenantInfo>> ListTenantsAsync(string tenant, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "console/tenants");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
request.Headers.Add("X-StellaOps-Tenant", tenant.Trim().ToLowerInvariant());
|
||||
}
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content
|
||||
.ReadFromJsonAsync<TenantListResponse>(cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return result?.Tenants ?? Array.Empty<TenantInfo>();
|
||||
}
|
||||
}
|
||||
17
src/Cli/StellaOps.Cli/Services/IAuthorityConsoleClient.cs
Normal file
17
src/Cli/StellaOps.Cli/Services/IAuthorityConsoleClient.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client for Authority console endpoints (CLI-TEN-47-001).
|
||||
/// </summary>
|
||||
internal interface IAuthorityConsoleClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Lists available tenants for the authenticated principal.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<TenantInfo>> ListTenantsAsync(string tenant, CancellationToken cancellationToken);
|
||||
}
|
||||
37
src/Cli/StellaOps.Cli/Services/Models/TenantModels.cs
Normal file
37
src/Cli/StellaOps.Cli/Services/Models/TenantModels.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Response from GET /console/tenants endpoint.
|
||||
/// </summary>
|
||||
internal sealed record TenantListResponse(
|
||||
[property: JsonPropertyName("tenants")] IReadOnlyList<TenantInfo> Tenants);
|
||||
|
||||
/// <summary>
|
||||
/// Tenant metadata as returned by the Authority service.
|
||||
/// </summary>
|
||||
internal sealed record TenantInfo(
|
||||
[property: JsonPropertyName("id")] string Id,
|
||||
[property: JsonPropertyName("displayName")] string DisplayName,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("isolationMode")] string IsolationMode,
|
||||
[property: JsonPropertyName("defaultRoles")] IReadOnlyList<string> DefaultRoles,
|
||||
[property: JsonPropertyName("projects")] IReadOnlyList<string> Projects);
|
||||
|
||||
/// <summary>
|
||||
/// Persistent tenant profile stored at ~/.stellaops/profile.json.
|
||||
/// </summary>
|
||||
internal sealed record TenantProfile
|
||||
{
|
||||
[JsonPropertyName("activeTenant")]
|
||||
public string? ActiveTenant { get; init; }
|
||||
|
||||
[JsonPropertyName("activeTenantDisplayName")]
|
||||
public string? ActiveTenantDisplayName { get; init; }
|
||||
|
||||
[JsonPropertyName("lastUpdated")]
|
||||
public DateTimeOffset? LastUpdated { get; init; }
|
||||
}
|
||||
137
src/Cli/StellaOps.Cli/Services/TenantProfileStore.cs
Normal file
137
src/Cli/StellaOps.Cli/Services/TenantProfileStore.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Stores and retrieves the active tenant profile at ~/.stellaops/profile.json.
|
||||
/// CLI-TEN-47-001: Persistent profiles implementation.
|
||||
/// </summary>
|
||||
internal static class TenantProfileStore
|
||||
{
|
||||
private const string ProfileFileName = "profile.json";
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public static string GetProfileDirectory()
|
||||
{
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (string.IsNullOrWhiteSpace(home))
|
||||
{
|
||||
home = AppContext.BaseDirectory;
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(home, ".stellaops"));
|
||||
}
|
||||
|
||||
public static string GetProfilePath()
|
||||
=> Path.Combine(GetProfileDirectory(), ProfileFileName);
|
||||
|
||||
public static async Task<TenantProfile?> LoadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var path = GetProfilePath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = File.OpenRead(path);
|
||||
return await JsonSerializer.DeserializeAsync<TenantProfile>(stream, JsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static TenantProfile? Load()
|
||||
{
|
||||
var path = GetProfilePath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonSerializer.Deserialize<TenantProfile>(json, JsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SaveAsync(TenantProfile profile, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(profile);
|
||||
|
||||
var directory = GetProfileDirectory();
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var path = GetProfilePath();
|
||||
await using var stream = File.Create(path);
|
||||
await JsonSerializer.SerializeAsync(stream, profile, JsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task SetActiveTenantAsync(string tenantId, string? displayName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var profile = new TenantProfile
|
||||
{
|
||||
ActiveTenant = tenantId?.Trim().ToLowerInvariant(),
|
||||
ActiveTenantDisplayName = displayName?.Trim(),
|
||||
LastUpdated = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await SaveAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task ClearActiveTenantAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var profile = new TenantProfile
|
||||
{
|
||||
ActiveTenant = null,
|
||||
ActiveTenantDisplayName = null,
|
||||
LastUpdated = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await SaveAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static string? GetEffectiveTenant(string? commandLineTenant)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(commandLineTenant))
|
||||
{
|
||||
return commandLineTenant.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
var envTenant = Environment.GetEnvironmentVariable("STELLAOPS_TENANT");
|
||||
if (!string.IsNullOrWhiteSpace(envTenant))
|
||||
{
|
||||
return envTenant.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
var profile = Load();
|
||||
return profile?.ActiveTenant;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user