---
title: SAIF.NetCore.EnvironmentClient security and authorization
summary: Authorization requirements, AD resource groups, security enforcement points, and data-level access control.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.EnvironmentClient
reference_type: package
---

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

```mermaid
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?**

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [API reference](api/index.md)

> **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`
