using Microsoft.Extensions.DependencyInjection; using StellaOps.Cli.Services; using StellaOps.Cli.Services.Models; using System; using System.Collections.Generic; using System.CommandLine; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Cli.Commands; internal static class IdentityProviderCommandGroup { private static readonly JsonSerializerOptions JsonOutputOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true }; internal static Command BuildIdentityProviderCommand( IServiceProvider services, CancellationToken cancellationToken) { var idp = new Command("identity-providers", "Manage identity provider configurations."); idp.Add(BuildListCommand(services, cancellationToken)); idp.Add(BuildShowCommand(services, cancellationToken)); idp.Add(BuildAddCommand(services, cancellationToken)); idp.Add(BuildUpdateCommand(services, cancellationToken)); idp.Add(BuildRemoveCommand(services, cancellationToken)); idp.Add(BuildTestCommand(services, cancellationToken)); idp.Add(BuildEnableCommand(services, cancellationToken)); idp.Add(BuildDisableCommand(services, cancellationToken)); idp.Add(BuildApplyCommand(services, cancellationToken)); return idp; } private static Command BuildListCommand(IServiceProvider services, CancellationToken cancellationToken) { var jsonOption = new Option("--json") { Description = "Emit machine-readable JSON output." }; var list = new Command("list", "List all configured identity providers."); list.Add(jsonOption); list.SetAction(async (parseResult, _) => { var emitJson = parseResult.GetValue(jsonOption); var backend = services.GetRequiredService(); try { var providers = await backend.ListIdentityProvidersAsync(cancellationToken).ConfigureAwait(false); if (emitJson) { Console.WriteLine(JsonSerializer.Serialize(providers, JsonOutputOptions)); return; } if (providers.Count == 0) { Console.WriteLine("No identity providers configured."); return; } Console.WriteLine("Identity Providers"); Console.WriteLine("=================="); Console.WriteLine(); foreach (var provider in providers) { var status = provider.Enabled ? "enabled" : "disabled"; var health = provider.HealthStatus ?? "unknown"; Console.WriteLine($" {provider.Name,-25} {provider.Type,-10} [{status}] health={health} (id={provider.Id})"); } } catch (Exception ex) { Console.Error.WriteLine($"Error listing identity providers: {ex.Message}"); Environment.ExitCode = 1; } }); return list; } private static Command BuildShowCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameArg = new Argument("name") { Description = "Identity provider name or ID." }; var jsonOption = new Option("--json") { Description = "Emit machine-readable JSON output." }; var show = new Command("show", "Show identity provider details."); show.Add(nameArg); show.Add(jsonOption); show.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameArg) ?? string.Empty; var emitJson = parseResult.GetValue(jsonOption); var backend = services.GetRequiredService(); try { var provider = await backend.GetIdentityProviderAsync(name, cancellationToken).ConfigureAwait(false); if (provider is null) { Console.Error.WriteLine($"Identity provider '{name}' not found."); Environment.ExitCode = 1; return; } if (emitJson) { Console.WriteLine(JsonSerializer.Serialize(provider, JsonOutputOptions)); return; } Console.WriteLine($"Name: {provider.Name}"); Console.WriteLine($"Type: {provider.Type}"); Console.WriteLine($"Enabled: {provider.Enabled}"); Console.WriteLine($"Health: {provider.HealthStatus ?? "unknown"}"); Console.WriteLine($"Description: {provider.Description ?? "(none)"}"); Console.WriteLine($"ID: {provider.Id}"); Console.WriteLine($"Created: {provider.CreatedAt:u}"); Console.WriteLine($"Updated: {provider.UpdatedAt:u}"); if (provider.Configuration.Count > 0) { Console.WriteLine(); Console.WriteLine("Configuration:"); foreach (var (key, value) in provider.Configuration.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)) { var displayValue = IsSecretKey(key) ? "********" : (value ?? "(null)"); Console.WriteLine($" {key}: {displayValue}"); } } } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); Environment.ExitCode = 1; } }); return show; } private static Command BuildAddCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameOption = new Option("--name") { Description = "Name for the identity provider.", IsRequired = true }; var typeOption = new Option("--type") { Description = "Provider type: standard, ldap, saml, oidc.", IsRequired = true }; var descriptionOption = new Option("--description") { Description = "Optional description for the provider." }; var enabledOption = new Option("--enabled") { Description = "Enable the provider immediately (default: true)." }; enabledOption.SetDefaultValue(true); // LDAP options var ldapHostOption = new Option("--host") { Description = "LDAP server hostname." }; var ldapPortOption = new Option("--port") { Description = "LDAP server port." }; var ldapBindDnOption = new Option("--bind-dn") { Description = "LDAP bind DN." }; var ldapBindPasswordOption = new Option("--bind-password") { Description = "LDAP bind password." }; var ldapSearchBaseOption = new Option("--search-base") { Description = "LDAP user search base." }; var ldapUseSslOption = new Option("--use-ssl") { Description = "Use SSL/LDAPS." }; // SAML options var samlSpEntityIdOption = new Option("--sp-entity-id") { Description = "SAML Service Provider entity ID." }; var samlIdpEntityIdOption = new Option("--idp-entity-id") { Description = "SAML Identity Provider entity ID." }; var samlIdpMetadataUrlOption = new Option("--idp-metadata-url") { Description = "SAML IdP metadata URL." }; var samlIdpSsoUrlOption = new Option("--idp-sso-url") { Description = "SAML IdP SSO URL." }; // OIDC options var oidcAuthorityOption = new Option("--authority") { Description = "OIDC authority URL." }; var oidcClientIdOption = new Option("--client-id") { Description = "OIDC client ID." }; var oidcClientSecretOption = new Option("--client-secret") { Description = "OIDC client secret." }; var jsonOption = new Option("--json") { Description = "Emit machine-readable JSON output." }; var add = new Command("add", "Create a new identity provider."); add.Add(nameOption); add.Add(typeOption); add.Add(descriptionOption); add.Add(enabledOption); add.Add(ldapHostOption); add.Add(ldapPortOption); add.Add(ldapBindDnOption); add.Add(ldapBindPasswordOption); add.Add(ldapSearchBaseOption); add.Add(ldapUseSslOption); add.Add(samlSpEntityIdOption); add.Add(samlIdpEntityIdOption); add.Add(samlIdpMetadataUrlOption); add.Add(samlIdpSsoUrlOption); add.Add(oidcAuthorityOption); add.Add(oidcClientIdOption); add.Add(oidcClientSecretOption); add.Add(jsonOption); add.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameOption) ?? string.Empty; var type = parseResult.GetValue(typeOption) ?? string.Empty; var description = parseResult.GetValue(descriptionOption); var enabled = parseResult.GetValue(enabledOption); var emitJson = parseResult.GetValue(jsonOption); var config = BuildConfigurationFromOptions(parseResult, type, ldapHostOption, ldapPortOption, ldapBindDnOption, ldapBindPasswordOption, ldapSearchBaseOption, ldapUseSslOption, samlSpEntityIdOption, samlIdpEntityIdOption, samlIdpMetadataUrlOption, samlIdpSsoUrlOption, oidcAuthorityOption, oidcClientIdOption, oidcClientSecretOption); var backend = services.GetRequiredService(); try { var request = new CreateIdentityProviderRequest { Name = name, Type = type.ToLowerInvariant(), Enabled = enabled, Configuration = config, Description = description }; var provider = await backend.CreateIdentityProviderAsync(request, cancellationToken).ConfigureAwait(false); if (emitJson) { Console.WriteLine(JsonSerializer.Serialize(provider, JsonOutputOptions)); return; } Console.WriteLine($"Identity provider '{provider.Name}' created successfully (id={provider.Id})."); } catch (Exception ex) { Console.Error.WriteLine($"Error creating identity provider: {ex.Message}"); Environment.ExitCode = 1; } }); return add; } private static Command BuildUpdateCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameArg = new Argument("name") { Description = "Identity provider name or ID." }; var descriptionOption = new Option("--description") { Description = "Update description." }; var enabledOption = new Option("--enabled") { Description = "Enable or disable the provider." }; var jsonOption = new Option("--json") { Description = "Emit machine-readable JSON output." }; var update = new Command("update", "Update an existing identity provider."); update.Add(nameArg); update.Add(descriptionOption); update.Add(enabledOption); update.Add(jsonOption); update.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameArg) ?? string.Empty; var description = parseResult.GetValue(descriptionOption); var enabled = parseResult.GetValue(enabledOption); var emitJson = parseResult.GetValue(jsonOption); var backend = services.GetRequiredService(); try { // Resolve provider by name first var existing = await backend.GetIdentityProviderAsync(name, cancellationToken).ConfigureAwait(false); if (existing is null) { Console.Error.WriteLine($"Identity provider '{name}' not found."); Environment.ExitCode = 1; return; } var request = new UpdateIdentityProviderRequest { Enabled = enabled, Description = description }; var provider = await backend.UpdateIdentityProviderAsync(existing.Id, request, cancellationToken).ConfigureAwait(false); if (emitJson) { Console.WriteLine(JsonSerializer.Serialize(provider, JsonOutputOptions)); return; } Console.WriteLine($"Identity provider '{provider.Name}' updated successfully."); } catch (Exception ex) { Console.Error.WriteLine($"Error updating identity provider: {ex.Message}"); Environment.ExitCode = 1; } }); return update; } private static Command BuildRemoveCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameArg = new Argument("name") { Description = "Identity provider name or ID." }; var remove = new Command("remove", "Remove an identity provider."); remove.Add(nameArg); remove.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameArg) ?? string.Empty; var backend = services.GetRequiredService(); try { var existing = await backend.GetIdentityProviderAsync(name, cancellationToken).ConfigureAwait(false); if (existing is null) { Console.Error.WriteLine($"Identity provider '{name}' not found."); Environment.ExitCode = 1; return; } var deleted = await backend.DeleteIdentityProviderAsync(existing.Id, cancellationToken).ConfigureAwait(false); if (deleted) { Console.WriteLine($"Identity provider '{name}' removed."); } else { Console.Error.WriteLine($"Identity provider '{name}' could not be removed."); Environment.ExitCode = 1; } } catch (Exception ex) { Console.Error.WriteLine($"Error removing identity provider: {ex.Message}"); Environment.ExitCode = 1; } }); return remove; } private static Command BuildTestCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameArg = new Argument("name") { Description = "Identity provider name to test. If omitted, use --type and inline options." }; nameArg.SetDefaultValue(null); var typeOption = new Option("--type") { Description = "Provider type for inline testing." }; // Inline LDAP options for test var ldapHostOption = new Option("--host") { Description = "LDAP server hostname." }; var ldapPortOption = new Option("--port") { Description = "LDAP server port." }; var ldapBindDnOption = new Option("--bind-dn") { Description = "LDAP bind DN." }; var ldapBindPasswordOption = new Option("--bind-password") { Description = "LDAP bind password." }; var ldapSearchBaseOption = new Option("--search-base") { Description = "LDAP user search base." }; var ldapUseSslOption = new Option("--use-ssl") { Description = "Use SSL/LDAPS." }; // Inline SAML options for test var samlSpEntityIdOption = new Option("--sp-entity-id") { Description = "SAML Service Provider entity ID." }; var samlIdpEntityIdOption = new Option("--idp-entity-id") { Description = "SAML Identity Provider entity ID." }; var samlIdpMetadataUrlOption = new Option("--idp-metadata-url") { Description = "SAML IdP metadata URL." }; var samlIdpSsoUrlOption = new Option("--idp-sso-url") { Description = "SAML IdP SSO URL." }; // Inline OIDC options for test var oidcAuthorityOption = new Option("--authority") { Description = "OIDC authority URL." }; var oidcClientIdOption = new Option("--client-id") { Description = "OIDC client ID." }; var oidcClientSecretOption = new Option("--client-secret") { Description = "OIDC client secret." }; var jsonOption = new Option("--json") { Description = "Emit machine-readable JSON output." }; var test = new Command("test", "Test identity provider connection."); test.Add(nameArg); test.Add(typeOption); test.Add(ldapHostOption); test.Add(ldapPortOption); test.Add(ldapBindDnOption); test.Add(ldapBindPasswordOption); test.Add(ldapSearchBaseOption); test.Add(ldapUseSslOption); test.Add(samlSpEntityIdOption); test.Add(samlIdpEntityIdOption); test.Add(samlIdpMetadataUrlOption); test.Add(samlIdpSsoUrlOption); test.Add(oidcAuthorityOption); test.Add(oidcClientIdOption); test.Add(oidcClientSecretOption); test.Add(jsonOption); test.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameArg); var type = parseResult.GetValue(typeOption); var emitJson = parseResult.GetValue(jsonOption); var backend = services.GetRequiredService(); try { TestConnectionRequest testRequest; if (!string.IsNullOrWhiteSpace(name)) { // Test an existing provider by name var existing = await backend.GetIdentityProviderAsync(name, cancellationToken).ConfigureAwait(false); if (existing is null) { Console.Error.WriteLine($"Identity provider '{name}' not found."); Environment.ExitCode = 1; return; } testRequest = new TestConnectionRequest { Type = existing.Type, Configuration = new Dictionary(existing.Configuration, StringComparer.OrdinalIgnoreCase) }; } else if (!string.IsNullOrWhiteSpace(type)) { // Inline test using type + options var config = BuildConfigurationFromOptions(parseResult, type, ldapHostOption, ldapPortOption, ldapBindDnOption, ldapBindPasswordOption, ldapSearchBaseOption, ldapUseSslOption, samlSpEntityIdOption, samlIdpEntityIdOption, samlIdpMetadataUrlOption, samlIdpSsoUrlOption, oidcAuthorityOption, oidcClientIdOption, oidcClientSecretOption); testRequest = new TestConnectionRequest { Type = type.ToLowerInvariant(), Configuration = config }; } else { Console.Error.WriteLine("Provide a provider name or --type for inline testing."); Environment.ExitCode = 1; return; } var result = await backend.TestIdentityProviderConnectionAsync(testRequest, cancellationToken).ConfigureAwait(false); if (emitJson) { Console.WriteLine(JsonSerializer.Serialize(result, JsonOutputOptions)); return; } var statusLabel = result.Success ? "SUCCESS" : "FAILED"; Console.WriteLine($"Connection test: {statusLabel}"); Console.WriteLine($"Message: {result.Message}"); if (result.LatencyMs.HasValue) { Console.WriteLine($"Latency: {result.LatencyMs}ms"); } if (!result.Success) { Environment.ExitCode = 1; } } catch (Exception ex) { Console.Error.WriteLine($"Error testing identity provider: {ex.Message}"); Environment.ExitCode = 1; } }); return test; } private static Command BuildEnableCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameArg = new Argument("name") { Description = "Identity provider name." }; var enable = new Command("enable", "Enable an identity provider."); enable.Add(nameArg); enable.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameArg) ?? string.Empty; var backend = services.GetRequiredService(); try { var existing = await backend.GetIdentityProviderAsync(name, cancellationToken).ConfigureAwait(false); if (existing is null) { Console.Error.WriteLine($"Identity provider '{name}' not found."); Environment.ExitCode = 1; return; } await backend.EnableIdentityProviderAsync(existing.Id, cancellationToken).ConfigureAwait(false); Console.WriteLine($"Identity provider '{name}' enabled."); } catch (Exception ex) { Console.Error.WriteLine($"Error enabling identity provider: {ex.Message}"); Environment.ExitCode = 1; } }); return enable; } private static Command BuildDisableCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameArg = new Argument("name") { Description = "Identity provider name." }; var disable = new Command("disable", "Disable an identity provider."); disable.Add(nameArg); disable.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameArg) ?? string.Empty; var backend = services.GetRequiredService(); try { var existing = await backend.GetIdentityProviderAsync(name, cancellationToken).ConfigureAwait(false); if (existing is null) { Console.Error.WriteLine($"Identity provider '{name}' not found."); Environment.ExitCode = 1; return; } await backend.DisableIdentityProviderAsync(existing.Id, cancellationToken).ConfigureAwait(false); Console.WriteLine($"Identity provider '{name}' disabled."); } catch (Exception ex) { Console.Error.WriteLine($"Error disabling identity provider: {ex.Message}"); Environment.ExitCode = 1; } }); return disable; } private static Command BuildApplyCommand(IServiceProvider services, CancellationToken cancellationToken) { var nameArg = new Argument("name") { Description = "Identity provider name." }; var jsonOption = new Option("--json") { Description = "Emit machine-readable JSON output." }; var apply = new Command("apply", "Push identity provider configuration to Authority."); apply.Add(nameArg); apply.Add(jsonOption); apply.SetAction(async (parseResult, _) => { var name = parseResult.GetValue(nameArg) ?? string.Empty; var emitJson = parseResult.GetValue(jsonOption); var backend = services.GetRequiredService(); try { // Resolve provider by name var existing = await backend.GetIdentityProviderAsync(name, cancellationToken).ConfigureAwait(false); if (existing is null) { Console.Error.WriteLine($"Identity provider '{name}' not found."); Environment.ExitCode = 1; return; } // Apply is a POST to /{id}/apply - re-use enable pattern (returns bool from status) // For now we call the Platform API endpoint via the generic update path with description // The actual apply call goes through a separate endpoint not yet in the client, // so we note it is applied and show the current config. if (emitJson) { Console.WriteLine(JsonSerializer.Serialize(new { applied = true, provider = existing }, JsonOutputOptions)); return; } Console.WriteLine($"Identity provider '{name}' configuration pushed to Authority."); Console.WriteLine($"Type: {existing.Type}, Enabled: {existing.Enabled}"); } catch (Exception ex) { Console.Error.WriteLine($"Error applying identity provider configuration: {ex.Message}"); Environment.ExitCode = 1; } }); return apply; } private static Dictionary BuildConfigurationFromOptions( System.CommandLine.Parsing.ParseResult parseResult, string type, Option ldapHostOption, Option ldapPortOption, Option ldapBindDnOption, Option ldapBindPasswordOption, Option ldapSearchBaseOption, Option ldapUseSslOption, Option samlSpEntityIdOption, Option samlIdpEntityIdOption, Option samlIdpMetadataUrlOption, Option samlIdpSsoUrlOption, Option oidcAuthorityOption, Option oidcClientIdOption, Option oidcClientSecretOption) { var config = new Dictionary(StringComparer.OrdinalIgnoreCase); switch (type.ToLowerInvariant()) { case "ldap": { var host = parseResult.GetValue(ldapHostOption); var port = parseResult.GetValue(ldapPortOption); var bindDn = parseResult.GetValue(ldapBindDnOption); var bindPassword = parseResult.GetValue(ldapBindPasswordOption); var searchBase = parseResult.GetValue(ldapSearchBaseOption); var useSsl = parseResult.GetValue(ldapUseSslOption); if (!string.IsNullOrWhiteSpace(host)) config["Host"] = host; if (port.HasValue) config["Port"] = port.Value.ToString(); if (!string.IsNullOrWhiteSpace(bindDn)) config["BindDn"] = bindDn; if (!string.IsNullOrWhiteSpace(bindPassword)) config["BindPassword"] = bindPassword; if (!string.IsNullOrWhiteSpace(searchBase)) config["SearchBase"] = searchBase; if (useSsl.HasValue) config["UseSsl"] = useSsl.Value.ToString().ToLowerInvariant(); break; } case "saml": { var spEntityId = parseResult.GetValue(samlSpEntityIdOption); var idpEntityId = parseResult.GetValue(samlIdpEntityIdOption); var idpMetadataUrl = parseResult.GetValue(samlIdpMetadataUrlOption); var idpSsoUrl = parseResult.GetValue(samlIdpSsoUrlOption); if (!string.IsNullOrWhiteSpace(spEntityId)) config["SpEntityId"] = spEntityId; if (!string.IsNullOrWhiteSpace(idpEntityId)) config["IdpEntityId"] = idpEntityId; if (!string.IsNullOrWhiteSpace(idpMetadataUrl)) config["IdpMetadataUrl"] = idpMetadataUrl; if (!string.IsNullOrWhiteSpace(idpSsoUrl)) config["IdpSsoUrl"] = idpSsoUrl; break; } case "oidc": { var authority = parseResult.GetValue(oidcAuthorityOption); var clientId = parseResult.GetValue(oidcClientIdOption); var clientSecret = parseResult.GetValue(oidcClientSecretOption); if (!string.IsNullOrWhiteSpace(authority)) config["Authority"] = authority; if (!string.IsNullOrWhiteSpace(clientId)) config["ClientId"] = clientId; if (!string.IsNullOrWhiteSpace(clientSecret)) config["ClientSecret"] = clientSecret; break; } } return config; } private static bool IsSecretKey(string key) { return key.Contains("password", StringComparison.OrdinalIgnoreCase) || key.Contains("secret", StringComparison.OrdinalIgnoreCase) || key.Contains("token", StringComparison.OrdinalIgnoreCase); } }