|
|
|
|
@@ -0,0 +1,220 @@
|
|
|
|
|
namespace StellaOps.Authority.Plugin.Unified;
|
|
|
|
|
|
|
|
|
|
using StellaOps.Authority.Plugins.Abstractions;
|
|
|
|
|
using StellaOps.Plugin.Abstractions;
|
|
|
|
|
using StellaOps.Plugin.Abstractions.Capabilities;
|
|
|
|
|
using StellaOps.Plugin.Abstractions.Context;
|
|
|
|
|
using StellaOps.Plugin.Abstractions.Health;
|
|
|
|
|
using StellaOps.Plugin.Abstractions.Lifecycle;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Adapts an existing IIdentityProviderPlugin to the unified IPlugin and IAuthCapability interfaces.
|
|
|
|
|
/// This enables gradual migration of Authority plugins to the unified plugin architecture.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class AuthPluginAdapter : IPlugin, IAuthCapability
|
|
|
|
|
{
|
|
|
|
|
private readonly IIdentityProviderPlugin _inner;
|
|
|
|
|
private IPluginContext? _context;
|
|
|
|
|
private PluginLifecycleState _state = PluginLifecycleState.Discovered;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a new adapter for an existing identity provider plugin.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="inner">The existing identity provider plugin to wrap.</param>
|
|
|
|
|
public AuthPluginAdapter(IIdentityProviderPlugin inner)
|
|
|
|
|
{
|
|
|
|
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public PluginInfo Info => new(
|
|
|
|
|
Id: $"com.stellaops.auth.{_inner.Type}",
|
|
|
|
|
Name: _inner.Name,
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
Vendor: "Stella Ops",
|
|
|
|
|
Description: $"Authority {_inner.Type} identity provider plugin");
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public PluginCapabilities Capabilities => PluginCapabilities.Auth | PluginCapabilities.Network;
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public PluginLifecycleState State => _state;
|
|
|
|
|
|
|
|
|
|
#region IAuthCapability
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public string ProviderType => _inner.Type;
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public IReadOnlyList<string> SupportedMethods
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
var methods = new List<string>();
|
|
|
|
|
if (_inner.Capabilities.SupportsPassword)
|
|
|
|
|
methods.Add("password");
|
|
|
|
|
if (_inner.Capabilities.SupportsMfa)
|
|
|
|
|
methods.Add("mfa");
|
|
|
|
|
return methods;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async Task<AuthResult> AuthenticateAsync(AuthRequest request, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
if (request.Method != "password" || string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))
|
|
|
|
|
{
|
|
|
|
|
return AuthResult.Failed("Invalid authentication method or missing credentials");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await _inner.Credentials.VerifyPasswordAsync(
|
|
|
|
|
request.Username,
|
|
|
|
|
request.Password,
|
|
|
|
|
ct);
|
|
|
|
|
|
|
|
|
|
if (result.Succeeded && result.User != null)
|
|
|
|
|
{
|
|
|
|
|
return AuthResult.Succeeded(
|
|
|
|
|
userId: result.User.SubjectId,
|
|
|
|
|
roles: result.User.Roles?.ToList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return AuthResult.Failed(result.Message ?? "Authentication failed");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return AuthResult.Failed(ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public Task<TokenValidationResult> ValidateTokenAsync(string token, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
// Authority plugins don't typically handle token validation directly
|
|
|
|
|
// This is handled by the Authority web service
|
|
|
|
|
return Task.FromResult(TokenValidationResult.Invalid("Token validation not supported by this provider"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async Task<AuthUserInfo?> GetUserInfoAsync(string userId, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var user = await _inner.Credentials.FindBySubjectAsync(userId, ct);
|
|
|
|
|
if (user == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
return new AuthUserInfo(
|
|
|
|
|
Id: user.SubjectId,
|
|
|
|
|
Username: user.Username,
|
|
|
|
|
Email: user.Attributes?.GetValueOrDefault("email"),
|
|
|
|
|
DisplayName: user.DisplayName,
|
|
|
|
|
Attributes: user.Attributes?.Where(kv => kv.Value != null)
|
|
|
|
|
.ToDictionary(kv => kv.Key, kv => kv.Value!));
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async Task<IReadOnlyList<AuthGroupInfo>> GetUserGroupsAsync(string userId, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Get user and extract roles as groups
|
|
|
|
|
var user = await _inner.Credentials.FindBySubjectAsync(userId, ct);
|
|
|
|
|
if (user == null)
|
|
|
|
|
return Array.Empty<AuthGroupInfo>();
|
|
|
|
|
|
|
|
|
|
return user.Roles.Select(role => new AuthGroupInfo(role, role, null)).ToList();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return Array.Empty<AuthGroupInfo>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async Task<bool> HasPermissionAsync(string userId, string permission, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var groups = await GetUserGroupsAsync(userId, ct);
|
|
|
|
|
return groups.Any(g => g.Name.Equals(permission, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public Task<SsoInitiation?> InitiateSsoAsync(SsoRequest request, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
// SSO is type-specific - LDAP doesn't support it, OIDC/SAML do
|
|
|
|
|
// This base adapter doesn't support SSO; specialized adapters should override
|
|
|
|
|
return Task.FromResult<SsoInitiation?>(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public Task<AuthResult> CompleteSsoAsync(SsoCallback callback, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(AuthResult.Failed("SSO not supported by this provider"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region IPlugin
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
_context = context;
|
|
|
|
|
_state = PluginLifecycleState.Initializing;
|
|
|
|
|
|
|
|
|
|
// The inner plugin is already initialized via the Authority plugin loader
|
|
|
|
|
// We just need to verify it's working
|
|
|
|
|
var health = await _inner.CheckHealthAsync(ct);
|
|
|
|
|
if (health.Status == AuthorityPluginHealthStatus.Unavailable)
|
|
|
|
|
{
|
|
|
|
|
_state = PluginLifecycleState.Failed;
|
|
|
|
|
throw new InvalidOperationException($"Authority plugin health check failed: {health.Message}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_state = PluginLifecycleState.Active;
|
|
|
|
|
context.Logger.Info("Authority plugin adapter initialized for {PluginName}", _inner.Name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await _inner.CheckHealthAsync(ct);
|
|
|
|
|
|
|
|
|
|
return result.Status switch
|
|
|
|
|
{
|
|
|
|
|
AuthorityPluginHealthStatus.Healthy => HealthCheckResult.Healthy()
|
|
|
|
|
.WithDetails(result.Details?.Where(kv => kv.Value != null)
|
|
|
|
|
.ToDictionary(kv => kv.Key, kv => (object)kv.Value!) ?? new Dictionary<string, object>()),
|
|
|
|
|
AuthorityPluginHealthStatus.Degraded => HealthCheckResult.Degraded(result.Message ?? "Degraded")
|
|
|
|
|
.WithDetails(result.Details?.Where(kv => kv.Value != null)
|
|
|
|
|
.ToDictionary(kv => kv.Key, kv => (object)kv.Value!) ?? new Dictionary<string, object>()),
|
|
|
|
|
_ => HealthCheckResult.Unhealthy(result.Message ?? "Unhealthy")
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return HealthCheckResult.Unhealthy(ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
_state = PluginLifecycleState.Stopped;
|
|
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|