Skip to content

SAIF.NetCore.EnvironmentClient security and authorization

This library performs machine authorization, not end-user authorization: it decides whether the computer it runs on may use developer-only configuration shortcuts, and it authorizes the calling application to the Environment API with a system hash for secure (crypto) setting retrieval. There is no centralized WCF SecurityService integration; the AD check is performed locally through System.DirectoryServices.AccountManagement.

Authorization architecture

Authorization is local machine check + remote per-secret authorization. AD computer-group membership is checked locally by this library; secret access is authorized by the Environment API (outside this repository) using a system hash this library presents as a query parameter.

flowchart TD
    Ctor["Client constructor<br>BaseEnvironmentClient..ctor → Setup()"] --> Resolve["HostResolver.ResolveHost"]
    Resolve --> EP1{"EP-1: override allowed<br>and machine in Developer_Machine_Users?"}
    EP1 -- "yes + ApplicationHost set" --> Dev["AppConfigOverride = true<br>(IsDeveloperMachine)"]
    EP1 -- "no" --> Path["Host from content path<br>or machine name"]
    Dev --> EP3["EP-3: developer override file enabled"]
    Dev --> EP4
    Path --> EP4{"EP-4 (SecureEnvironmentClient only):<br>COREAPIENABLED=1 or developer machine?"}
    EP4 -- "no" --> Deny1["EnvironmentClientException<br>not enabled for CORE APIs"]
    EP4 -- "yes" --> Call["GetSecureSetting*"]
    Call --> Cache{"DL-1: cached by<br>section.key?"}
    Cache -- "hit" --> Return["Return cached secret<br>(hash not rechecked)"]
    Cache -- "miss or stale" --> EP6["EP-6: GET /v2/secureSetting/...<br>?systemHashValue=hash"]
    EP6 --> Server["Environment API validates hash<br>(outside repository)"]
    Base["BaseEnvironmentClient<br>GetSecureSetting*"] --> EP5["EP-5: always throws<br>Operation not allowed"]

AD resource catalog

One AD group is referenced. It is a computer group, not a user resource, and does not follow the SAIF <ResourceName>Resource/<ResourceName>TestResource naming convention, so no environment suffix is applied.

Resource name Context Enforcement point
Developer_Machine_Users AD computer group. Membership of the running machine's computer account marks the machine as a developer workstation, permitting the AppSettings:ApplicationHost host/track override, the local developer settings override file, and construction of SecureEnvironmentClient without COREAPIENABLED=1. HostResolver.IsConfigOverrideAllowed → HostResolver.AppConfigOverride → BaseEnvironmentClient.IsDeveloperMachine()

AD lookup mechanics

Aspect Value
API System.DirectoryServices.AccountManagement
Context new PrincipalContext(ContextType.Domain) with no domain name or container, so it binds to the domain of the current process identity / machine*
Principal looked up ComputerPrincipal.FindByIdentity(context, GetComputerName()) — the machine's computer account, not the user
Group GroupPrincipal.FindByIdentity(context, "Developer_Machine_Users") — hard-coded literal
Membership test computer.IsMemberOf(devGroup); a null computer or null group yields false. Nested groups are evaluated per .NET semantics*
Pre-condition Lookup runs only when AppSettings:EnvApiDeveloperOverrideAllowed is empty or parses as true; an unparsable value sets false, which skips the lookup
Failure handling No try/catch. An unreachable domain controller raises an exception out of ResolveHost, which fails Setup() and therefore the constructor*
Domain No corp.saif.com/external.saif.com literal or LDAP string; the domain is implicit

Not applicable: no SecurityService proxy, securityServiceUrl key, or /WCF/Security endpoint exists.

Security interfaces and contracts

Interface/type Namespace Role in this library
IEnvironmentClient SAIF.NetCore.Services.Contracts The public contract implemented by BaseEnvironmentClient/SecureEnvironmentClient. Exposes IsDeveloperMachine() (EP-2) and the full family of GetSecureSetting*/GetSecureSettingAsync* overloads (both systemHashValue-based and SystemType-based) that route through EP-5/EP-6, plus the non-secure GetSettingValue*/GetSectionSettings/HasSetting members.
ISystemHashService SAIF.NetCore.Services.Contracts Injected dependency used only by the SystemType-based GetSecureSetting* overloads. Get(SystemType type) returns the calling application's hash. RegisterEnvironmentClient supplies this as null; RegisterSecureEnvironmentClient supplies a real SystemHashService. The interface also exposes ComputeHash, ComputeHashNow, GetAssembly, and RemoveSystemHashSaltAndValidateLifetime, none of which this library calls directly.
SystemType (enum) SAIF.NetCore.Services.Contracts Classifies the calling application (Console, WindowsService, WebApp, Guidewire, ThickClient, Script, Other) for hash generation via ISystemHashService.Get. This library does not interpret the enum value itself; it is opaque input to the hash service.

Authorization enforcement points

Attribute-based

None. The library has no controllers, actions, or authorization attributes.

Programmatic

ID Enforcement point What it protects Condition to pass Effect when denied Source
EP-1 HostResolver.IsConfigOverrideAllowed (private), called first in HostResolver.ResolveHost Use of AppSettings:ApplicationHost to choose the host, and therefore the environment track whose settings are downloaded EnvApiDeveloperOverrideAllowed not false and machine is in Developer_Machine_Users Config host ignored; host resolved from content-root path or machine name src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/HostResolver.cs
EP-2 BaseEnvironmentClient.IsDeveloperMachine() (public virtual, part of IEnvironmentClient) Exposes the developer-machine decision to EP-3, EP-4, and consumers Returns HostResolver.AppConfigOverride, true only when EP-1 passes and AppSettings:ApplicationHost is non-empty Returns false src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/BaseEnvironmentClient.cs
EP-3 BaseEnvironmentClient.ResolveDeveloperSettingsOverride (private, from Setup) Activation of the local JSON override file named by AppSettings:EnvApiDeveloperSettingsOverride, which replaces any non-secure setting value File exists and IsDeveloperMachine() Override file not used src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/BaseEnvironmentClient.cs
EP-4 SecureEnvironmentClient constructor gate Creation of the only client type able to retrieve secure (crypto) settings config["COREAPIENABLED"] == "1" or IsDeveloperMachine() EnvironmentClientException("This server is not enabled for CORE APIs.") src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SecureEnvironmentClient.cs
EP-5 BaseEnvironmentClient.RetrieveSecureSetting<T>/RetrieveSecureSettingAsync<T> (protected virtual) All 16 public GetSecureSetting* overloads on the base client Never passes in the base class; only an override (SecureEnvironmentClient) can return a value EnvironmentClientException("Operation not allowed! This application is restricted from retrieving secure settings.") src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/BaseEnvironmentClient.cs
EP-6 SecureEnvironmentClient.RetrieveSecureSetting[Async] → GetSecureSettingUrlPart Each secure setting value The caller supplies, or ISystemHashService.Get(SystemType) computes, a systemHashValue sent as a query parameter to GET /v2/secureSetting/{app}/{track}[/{section}]/{key}/value; the Environment API decides whether the hash authorizes the secret. The client also requires the key to exist in the non-secure settings list (HasSetting) before calling Non-success HTTP result → EnvironmentClientException (404 → "could not find a value", other → "Failed to retrieve secure setting"), or the transport exception is rethrown src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SecureEnvironmentClient.cs

Caller-identity scheme (system hash)

Scheme Credential Presented as Validated by
System hash Opaque hash identifying the calling application, produced by ISystemHashService.Get(SystemType) or passed in by the caller. For consumers using RegisterSecureEnvironmentClient(type), the service is constructed from the consumer's entry assembly systemHashValue URL query parameter (URL-encoded) Environment API (outside this repository)

How consumers select the gate

Registration IEnvironmentClient implementation ISystemHashService Secure settings available
RegisterEnvironmentClient BaseEnvironmentClient factory returning null No — EP-5 always denies; SystemType overloads throw NullReferenceException before reaching EP-5*
RegisterSecureEnvironmentClient(type) SecureEnvironmentClient SystemHashService(Assembly.GetAssembly(type)) Yes, subject to EP-4 and EP-6

Data-level authorization findings

There is no entity-ownership check in this library. Per-secret authorization is delegated to the Environment API through the system hash (EP-6). One client-side behavior weakens that per-secret model.

Finding Severity
The secure-setting cache ignores the system hash. SecureSettingCache is keyed by "{section}.{configKey}" only. After one call fetches a secret with hash A, any later call in the same process for the same section and key is served from the cache, whatever hash it passes (including an invalid or empty one), until LastRefreshed advances. The server is not asked to re-authorize the second caller. Because the client is registered as a singleton per application, this matters only when code inside one application passes different hashes; it does not cross application boundaries* Medium
The secure fetch is pre-filtered by HasSetting(configKey, section), which consults the non-secure settings dictionary for the host. If the key is not listed there, no request is made and the caller gets "No crypto data found". The secret's existence is therefore disclosed by, and dependent on, the non-secure settings feed. Informational

Segregation of duties findings

None. No SOD classes, privilege-combination checks, or role/menu-structure queries exist.

Machine/application authorization findings

Check Principal Mechanism Grants
Developer-machine check Computer account named by Environment.MachineName (through ISystemWrapper.GetComputerName) ComputerPrincipal.IsMemberOf(GroupPrincipal "Developer_Machine_Users"); does not strip a $ suffix Host/track override (EP-1), developer override file (EP-3), and bypass of COREAPIENABLED (EP-4)
Server enablement flag Configuration of the host IConfiguration["COREAPIENABLED"] == "1". XML doc and comment describe this as the ASPNETCORE_COREAPIENABLED environment variable, but the code reads IConfiguration, so any configuration source can satisfy it Construction of SecureEnvironmentClient (EP-4)
Application identity Consuming application systemHashValue (EP-6), and IHttpJsonClient.SetClientInfo on every request Secure-setting retrieval; per-app/per-host settings

Security architecture summary

Metric Value
Authorization architecture Local machine check + remote per-secret authorization; no centralized WCF SecurityService
AD resource names referenced 1 (Developer_Machine_Users, computer group)
Authorization enforcement points 6 programmatic (EP-1 to EP-6); 0 attribute-based
Data-level authorization checks 0 local ownership checks; 1 remote per-secret check (EP-6); 1 cache-partitioning finding
SOD rules 0
Machine authorization present Yes (AD computer group + COREAPIENABLED flag + system hash)
Security contracts Consumes ISystemHashService, SystemType (SAIF.NetCore.ServiceContracts); exposes IsDeveloperMachine() via IEnvironmentClient

Notes

  • Secure-setting cache is not partitioned by system hash (see Data-level authorization findings)*.
  • The system hash travels in the URL query string, so it can be recorded in proxy, web-server, and HTTP-client logs, and in any trace that captures the request URL*.
  • EP-4 runs after the base constructor has already called Setup(), which contacts the Environment API and downloads the track list from a non-enabled server before the gate throws.
  • The COREAPIENABLED gate reads IConfiguration, not only an environment variable, so an appsettings entry is sufficient to enable secret retrieval; the constructor comment ("or you must be debugging in visual studio to have the environment variable VisualStudioDir") does not match the code, which uses IsDeveloperMachine().
  • A machine in Developer_Machine_Users can point itself at any track by setting AppSettings:ApplicationHost, and can replace any non-secure setting (for example service URLs) through the override file; secure settings are not overridable by the file, and their release still depends on the server-side hash check.
  • EnvApiDeveloperOverrideAllowed defaults to allowed when absent, so production servers rely on not being members of Developer_Machine_Users rather than on an explicit opt-out.
  • Plaintext secrets are held for the process lifetime in SecureSettingCacheItem.Value with no eviction other than ClearCache() or refresh, and ClearCache() is never invoked by this library.
  • Secure-setting telemetry spans tag settings.section and settings.key (not the value or hash), so secret key names reach the tracing backend.
  • With RegisterEnvironmentClient, the SystemType overloads throw NullReferenceException (null ISystemHashService) instead of the intended EP-5 "Operation not allowed!" message; the call is still denied*.

Open questions

  • How does the Environment API validate systemHashValue (lifetime, salt, per-application binding, per-secret ACL)? The server is not in this repository.**
  • Which headers and credentials does IHttpJsonClient.SetClientInfo send? The implementation is supplied by the consumer.**
  • Which domain does PrincipalContext(ContextType.Domain) bind to on a non-domain or local service account, and does the lookup work on non-Windows hosts (net8.0/net10.0 targets)?**
  • Who governs membership of Developer_Machine_Users, and is it guaranteed that no server computer account is a member?**
  • Is the hash-agnostic secure cache intentional?**

Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts. Statements marked with ** could not be confirmed; treat as unknown until verified.

Sources

  • src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/HostResolver.cs
  • src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SecureEnvironmentClient.cs
  • src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SecureSettingCache.cs
  • src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/BaseEnvironmentClient.cs
  • src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SystemWrapper.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/ISystemHashService.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/Enums.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/IEnvironmentClient.cs
  • src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs
  • src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/CoreAPIStartup.cs