API reference — Migration & configuration

Nuvora.Nexus.Sentinel.DeclarativeConfig

Declarative realm configuration for Sentinel: a versioned YAML/JSON model for realms, organizations, roles, OIDC clients, SAML connections, identity providers, workload trusts and webhook endpoints, with an idempotent diff-aware applier over the public store ports.

dotnet add package Nuvora.Nexus.Sentinel.DeclarativeConfig

Nuvora.Nexus.Sentinel.DeclarativeConfig

ConfigChangeKind

public enum ConfigChangeKind

Provides the base class for enumerations.

Values

  • Create — Entity absent from current state; created (or, DryRun, would be).
  • Update — Entity exists and drifts on updatable fields; updated (or, DryRun, would be).
  • Unchanged — Declared state matches current state — a no-op.
  • WouldPrune — Exists in current state, absent from the file, and the section opted into prune. REPORTED ONLY: v1 never deletes (absence ≠ deletion) — delete via the admin API.
  • UpdateUnsupported — Drift on an entity whose store port cannot apply it. No shipped port produces this any more (the create-only port gaps closed); the member remains for report consumers and for hosts that plug their own create-only stores into the applier seam.
  • Skipped — Entry not applied (e.g. its secretRef resolved to nothing); Detail says why.
  • Error — Entry unapplicable (unknown org reference, missing store registration); Detail says why.

ConfigDiffEntry

public sealed record ConfigDiffEntry : IEquatable<ConfigDiffEntry>

Key is the entity’s natural key within Section of realm RealmKey.

Constructors

ConfigDiffEntry(string Section, string RealmKey, string Key, ConfigChangeKind Kind, string? Detail = null)

Key is the entity’s natural key within Section of realm RealmKey.

Properties

ConfigChangeKind Kind { get; init; }
string Key { get; init; }
string RealmKey { get; init; }
string Section { get; init; }
string? Detail { get; init; }

Methods

override string ToString()

Returns a string that represents the current object.

ConfigDiffReport

public sealed class ConfigDiffReport

What one DeclarativeConfigApplier.ApplyAsync pass did — or, under ConfigDiffReport.DryRun, would do. Same shape either way, so operators can diff-preview with confidence that the real pass follows the preview.

Properties

List<ConfigDiffEntry> Entries { get; }
List<ConfigValidationIssue> ValidationWarnings { get; }

Validation output of the pre-apply pass (warnings only — errors abort before a report exists).

bool HasErrors { get; }
bool IsNoOp { get; }

True when the pass changed nothing (and would change nothing) — the idempotent re-apply signal.

int Creates { get; }
int Unchanged { get; }
int Updates { get; }
required bool DryRun { get; init; }

Methods

override string ToString()

Returns a string that represents the current object.

ConfigIssueSeverity

public enum ConfigIssueSeverity

Provides the base class for enumerations.

Values

  • Warning — Apply proceeds; the issue is surfaced (e.g. an unresolvable secretRef — that entry is skipped).
  • Error — Apply refuses to run (fail-closed posture): fix the file.

ConfigValidationIssue

public sealed record ConfigValidationIssue : IEquatable<ConfigValidationIssue>

Path is a human locator like realms[acme].roles[admin].grants[0].

Constructors

ConfigValidationIssue(ConfigIssueSeverity Severity, string Path, string Message)

Path is a human locator like realms[acme].roles[admin].grants[0].

Properties

ConfigIssueSeverity Severity { get; init; }
string Message { get; init; }
string Path { get; init; }

Methods

override string ToString()

Returns a string that represents the current object.

DeclarativeConfigApplier

public sealed class DeclarativeConfigApplier

Applies a declarative config against live state: idempotent and diff-aware — current state is loaded through the PUBLIC store ports, matched by natural keys, and only genuine drift produces writes; re-applying an already-applied file is a strict no-op.

NEVER deletes: absence from the file is not deletion. Sections opting into prune get ConfigChangeKind.WouldPrune REPORTING of what a future prune would remove; v1 refuses to auto-delete.

Every dependency is a public port an embedding app composes the same way: realms, domains and roles ride IAdminStore, OIDC clients ride the IOidcStore registry, and the federation/SAML/workload ports apply drift through their own update methods.

Constructors

DeclarativeConfigApplier(IAdminStore adminStore, PasswordHasher passwordHasher, ISentinelClock clock, ISecretResolver? secrets = null, IOidcStore? oidcStore = null, IIdentityProviderStore? identityProviders = null, ISamlStore? samlStore = null, IWorkloadTrustStore? workloadTrusts = null, IWebhookStore? webhookStore = null, IMachineIdentityStore? machineIdentities = null)

Applies a declarative config against live state: idempotent and diff-aware — current state is loaded through the PUBLIC store ports, matched by natural keys, and only genuine drift produces writes; re-applying an already-applied file is a strict no-op. NEVER deletes: absence from the file is not deletion. Sections opting into prune get ConfigChangeKind.WouldPrune REPORTING of what a future prune would remove; v1 refuses to auto-delete. Every dependency is a public port an embedding app composes the same way: realms, domains and roles ride IAdminStore, OIDC clients ride the IOidcStore registry, and the federation/SAML/workload ports apply drift through their own update methods.

Methods

Task<ConfigDiffReport> ApplyAsync(SentinelDeclarativeConfig config, bool dryRun = false, CancellationToken ct = default(CancellationToken))

Validates then applies. Validation ERRORS throw DeclarativeConfigValidationException before any write; warnings ride on the report. With dryRun the exact same matching and diffing runs but nothing is persisted.

DeclarativeConfigParseException

public sealed class DeclarativeConfigParseException : Exception

The declarative config file could not be parsed (malformed YAML/JSON or an unknown property).

Constructors

DeclarativeConfigParseException(string message, Exception? inner = null)

DeclarativeConfigValidationException

public sealed class DeclarativeConfigValidationException : Exception

The config failed validation with at least one ConfigIssueSeverity.Error — nothing was applied.

Constructors

DeclarativeConfigValidationException(IReadOnlyList<ConfigValidationIssue> issues)

The config failed validation with at least one ConfigIssueSeverity.Error — nothing was applied.

Properties

IReadOnlyList<ConfigValidationIssue> Issues { get; }

EnvironmentSecretResolver

public sealed class EnvironmentSecretResolver : ISecretResolver

Environment-variable resolver: secretRef is the variable name. Empty counts as missing.

Methods

string? Resolve(string secretRef)

The secret value, or null when the reference resolves to nothing (validation warns, apply skips).

Fields

static readonly EnvironmentSecretResolver Instance

GrantConfig

public sealed class GrantConfig

Properties

GrantEffect Effect { get; set; }
string? ConditionJson { get; set; }

Raw serialized grant-condition document; null = unconditional. Passed through opaquely.

string? Pattern { get; set; }

Permission pattern (service:scope:action, single-* wildcards). Validated before apply.

ISecretResolver

public interface ISecretResolver

Resolves a secretRef (a NAME, never a value) from the declarative config to the actual secret at apply time. The default reads environment variables; hosts with a secret manager register their own.

Methods

string? Resolve(string secretRef)

The secret value, or null when the reference resolves to nothing (validation warns, apply skips).

IdentityProviderEntry

public sealed class IdentityProviderEntry

Properties

JitProvisioningMode JitMode { get; set; }
List<string> Scopes { get; set; }
string? AuthorizeEndpoint { get; set; }
string? ClientId { get; set; }

The client_id Sentinel is registered as at the provider.

string? DisplayName { get; set; }
string? Issuer { get; set; }

The provider’s exact iss / OIDC discovery base.

string? JwksUri { get; set; }
string? Key { get; set; }

Natural key within the realm (IdentityProviderConfig.Key); appears in login URLs.

string? SecretRef { get; set; }

Env-var NAME holding the provider client secret. Omit for pure-PKCE public-client registrations.

string? TokenEndpoint { get; set; }

OidcClientConfig

public sealed class OidcClientConfig

Properties

List<string> AllowedScopes { get; set; }
List<string> PostLogoutRedirectUris { get; set; }
List<string> RedirectUris { get; set; }
OidcClientType Type { get; set; }
TimeSpan? AccessTokenLifetime { get; set; }

Per-client access-token lifetime override (e.g. 00:05:00); null = realm default.

bool FirstParty { get; set; }
bool RequireConsent { get; set; }
string? Audience { get; set; }

Access-token aud override; null = the client id itself.

string? BackChannelLogoutUri { get; set; }
string? ClientId { get; set; }

The wire client_id — the natural key (OidcClient.ClientId).

string? SecretRef { get; set; }

Env-var NAME holding the plaintext client secret (confidential clients). Hashed on apply; never stored in config.

OrganizationConfig

public sealed class OrganizationConfig

Properties

List<OrganizationDomainConfig> Domains { get; set; }

Login-routing domains. Matched by (value, kind); the verified flag is the one updatable field.

string? DisplayName { get; set; }
string? Key { get; set; }

Natural key within the realm (Organization.Key).

OrganizationDomainConfig

public sealed class OrganizationDomainConfig

Properties

OrganizationDomainKind Kind { get; set; }
bool Verified { get; set; }

Declaring verified: true skips domain verification — the config author is the deployment operator, the same trust root that would approve verification. Defaults false: unverified domains never route logins.

string? Domain { get; set; }

Lowercase email domain (acme.com) or subdomain label, per OrganizationDomainConfig.Kind.

PruneFlags

public sealed class PruneFlags

See RealmConfig.Prune: reporting-only in v1, never destructive.

Properties

bool IdentityProviders { get; set; }
bool OidcClients { get; set; }
bool Organizations { get; set; }
bool Roles { get; set; }
bool SamlIdpConnections { get; set; }
bool SamlSpConnections { get; set; }
bool Webhooks { get; set; }
bool WorkloadTrusts { get; set; }

RealmConfig

public sealed class RealmConfig

Properties

List<IdentityProviderEntry> IdentityProviders { get; set; }
List<OidcClientConfig> OidcClients { get; set; }
List<OrganizationConfig> Organizations { get; set; }
List<RoleConfig> Roles { get; set; }
List<SamlIdpConnectionConfig> SamlIdpConnections { get; set; }

SP-side connections to external SAML IdPs — Sentinel authenticates AGAINST these.

List<SamlSpConnectionConfig> SamlSpConnections { get; set; }

IdP-side registry of relying SPs — these SSO against Sentinel.

List<WebhookEndpointConfig> Webhooks { get; set; }
List<WorkloadTrustEntry> WorkloadTrusts { get; set; }
PruneFlags Prune { get; set; }

Per-section prune opt-in. Absence of an entity from the file NEVER deletes it; with a section’s flag set the diff REPORTS what would be pruned (ConfigChangeKind.WouldPrune) and v1 refuses to auto-delete — deletion stays an explicit admin-API act.

bool IsDefault { get; set; }

Marks this the deployment’s default realm (Realm.IsDefault). Applied at creation only.

string? DisplayName { get; set; }
string? Key { get; set; }

Stable machine key — the natural key diffs match on (Realm.Key).

RoleConfig

public sealed class RoleConfig

Properties

List<GrantConfig> Grants { get; set; }
string? DisplayName { get; set; }
string? Key { get; set; }

Natural key within (realm, organization) — Role.Key.

string? Organization { get; set; }

Org key for an org-local role; null declares a realm-level role.

SamlAttributeMappingConfig

public sealed record SamlAttributeMappingConfig : IEquatable<SamlAttributeMappingConfig>

Defines a generalized method that a value type or class implements to create a type-specific method for determining equality of instances.

Properties

string? Claim { get; set; }
string? SamlAttribute { get; set; }

SamlIdpConnectionConfig

public sealed class SamlIdpConnectionConfig

Properties

JitProvisioningMode JitMode { get; set; }
bool AllowIdpInitiated { get; set; }
string? DisplayName { get; set; }
string? IdpCertificatePem { get; set; }

Pinned signature-verification certificate, inline PEM (certificates are public material — no secretRef).

string? IdpEntityId { get; set; }
string? IdpSsoUrl { get; set; }
string? Key { get; set; }

Natural key within the realm (SamlIdpConnection.Key).

string? SpEntityId { get; set; }

SamlSpConnectionConfig

public sealed class SamlSpConnectionConfig

Properties

List<SamlAttributeMappingConfig> AttributeMappings { get; set; }
bool RequireSignedRequests { get; set; }
string? AcsUrl { get; set; }
string? Audience { get; set; }
string? SpCertificatePem { get; set; }
string? SpEntityId { get; set; }

Natural key within the realm (SamlSpConnection.SpEntityId).

SentinelConfigParser

public static class SentinelConfigParser

Parses declarative config files. Both formats are STRICT about unknown properties — a typo’d section name must fail the boot, not silently configure nothing. Requiredness/shape rules live in SentinelConfigValidator, not here.

Methods

static SentinelDeclarativeConfig ParseFile(string path)

Dispatches on extension: .json is JSON, everything else (.yaml/.yml) YAML.

static SentinelDeclarativeConfig ParseJson(string json)
static SentinelDeclarativeConfig ParseYaml(string yaml)

SentinelConfigValidator

public static class SentinelConfigValidator

Validates a parsed declarative config before it is applied: required natural keys, unique keys, permission patterns, absolute URLs, and secretRef resolvability (missing env vars are WARNINGS — the entry is skipped at apply, everything else proceeds). Cross-checks against live state (unknown org references, drift) are the applier’s job.

Methods

static IReadOnlyList<ConfigValidationIssue> Validate(SentinelDeclarativeConfig config, ISecretResolver? secrets = null)

SentinelDeclarativeConfig

public sealed class SentinelDeclarativeConfig

The versioned declarative realm configuration: YAML/JSON declaring the STRUCTURE of a deployment — realms, organizations (+domains), roles (+grants), OIDC clients, SAML connections (both directions), inbound identity providers, workload trusts, and webhook endpoints. Deliberately NOT people: users are runtime data (SCIM, JIT, imports), not config.

Secrets never live in the file: every secret-bearing entry carries a secretRef — the NAME of an environment variable resolved at apply time via ISecretResolver.

Properties are nullable-lax on purpose: parsing is shape-only, and SentinelConfigValidator is the single place that decides what is required — one error report instead of a serializer exception per missing field.

Properties

List<RealmConfig> Realms { get; set; }
int Version { get; set; }

Config-format version; v1 is the only one. Unknown versions fail validation, not parsing.

WebhookEndpointConfig

public sealed class WebhookEndpointConfig

Properties

List<string> Events { get; set; }

Subscription patterns over event kinds: exact or single-* wildcard (login.*).

string? Organization { get; set; }

Org key for an org-scoped endpoint (org fencing); null = realm-level.

string? SecretRef { get; set; }

Env-var NAME holding the HMAC signing secret. Required — an endpoint without a secret cannot sign.

string? Url { get; set; }

Absolute http(s) delivery URL — with WebhookEndpointConfig.Organization, the natural key.

WorkloadTrustEntry

public sealed class WorkloadTrustEntry

Properties

Dictionary<string, string> ClaimRules { get; set; }

Claim name → required value (exact or single-* wildcard); ALL must hold.

Guid? ServiceAccountId { get; set; }

The service account exchanged tokens act as, by id. Prefer WorkloadTrustEntry.ServiceAccount (the natural key) — the id form remains for configs written before the key lookup existed and for cross-realm tooling that already holds ids.

string? Audience { get; set; }
string? Issuer { get; set; }

External issuer (iss). Part of the natural key (issuer, audience, subjectPattern).

string? JwksUri { get; set; }
string? ServiceAccount { get; set; }

The service account by its realm-scoped machine KEY (e.g. ci-deployer) — resolved at apply time via IMachineIdentityStore.FindServiceAccountByKeyAsync. Exactly one of this and WorkloadTrustEntry.ServiceAccountId must be set.

string? SubjectPattern { get; set; }

Exact or single-* wildcard the external sub must match.

Nuvora.Nexus.Sentinel.DeclarativeConfig.DependencyInjection

DeclarativeConfigServiceCollectionExtensions

public static class DeclarativeConfigServiceCollectionExtensions

Registration for the declarative-config applier. Same DI posture as every Sentinel package: TryAdd everywhere, host registrations win.

Methods

static IServiceCollection AddSentinelDeclarativeConfig(this IServiceCollection services)

Registers ISecretResolver (environment variables by default) and a scoped DeclarativeConfigApplier. Call AFTER the stores it applies through: AddSentinelEfCoreStores + AddSentinelEfCoreAdminStore (+ the SAML / workload-trust / federation stores and webhooks as used), plus AddSentinel() for the password hasher and clock. Optional ports resolve leniently — a host that mounts no SAML surface can still apply a config without SAML sections.