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 intoprune. 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.