Class ScimProvisioningTrace

Namespace
Kuestenlogik.Bowire.Scim
Assembly
Kuestenlogik.Bowire.Scim.dll

Records what an identity provider's connector actually did, one line of JSON per SCIM request (#639).

public sealed class ScimProvisioningTrace
Inheritance
ScimProvisioningTrace
Inherited Members

Remarks

scim/events.jsonl already records every mutation, but it records the outcome — action, id, name, active. That answers "what is the state now", and #639 asks something else: what does a real connector do on the wire. Those questions have no overlap. The event log is written only from mutations, so a provider's reads — the paging walk, the existence filter, the query it uses to decide whether a deactivated user still exists — leave no trace at all, and they are most of what a live round-trip is for.

So this records the request, not the result: method, path and query (paging and filters become visible), the status returned, how long it took, and for PATCH the dialect the connector actually used. Okta sends lower-case op with a path; Entra ID capitalises Op and for deactivation omits the path. Bowire accepts both, and until something writes down which one arrived, "we handle both" stays a claim about a fixture.

It also names the attributes a payload carried that Bowire does not model. Those are kept verbatim and handed back on the next GET, which is correct and completely silent — an unmodelled attribute that matters would otherwise be discovered by a customer, not by us.

Off by default. This writes what the connector sent: user names, e-mail addresses, the filters a directory walk used. That is personal data about people who never agreed to be in a debug file, and it is wanted for a bounded exercise — a provisioning round-trip — not for normal operation. Turning it on is a decision, not a default.

Constructors

ScimProvisioningTrace(string, TimeProvider?)

Create a trace writing to path.

public ScimProvisioningTrace(string path, TimeProvider? clock = null)

Parameters

path string
clock TimeProvider

Remarks

The path is resolved here rather than on first write, so a configuration mistake surfaces at startup where an operator can act on it — instead of silently dropping every line of the exercise it was turned on for.

Properties

FilePath

Where the lines are written.

public string FilePath { get; }

Property Value

string

Remarks

Named FilePath rather than Path so the JSON member name "Path" below — which is Entra's capitalisation of a PATCH operation's path, and has nothing to do with this property — is not read as a stale reference to it.

Methods

DetectPatchDialect(string)

Which connector's PATCH shape this document is, or null when it is not a PATCH or cannot be read.

public static string? DetectPatchDialect(string body)

Parameters

body string

Returns

string

Remarks

Reported as observed, never guessed: the two shapes are told apart by what is actually on the wire — the casing of the op member and whether a path is present — not by a User-Agent, which a proxy may rewrite and a test harness will not send at all.

A document that matches neither is reported as other rather than forced into one of them. That is the interesting case: it means a third connector, or a version that changed its mind, and calling it Okta would bury exactly the finding this exists to surface.

Record(string, string, string?, int, double, string?, IReadOnlyCollection<string>?)

Record one request.

public void Record(string method, string path, string? query, int status, double elapsedMs, string? patchDialect = null, IReadOnlyCollection<string>? unmodelledAttributes = null)

Parameters

method string
path string
query string
status int
elapsedMs double
patchDialect string
unmodelledAttributes IReadOnlyCollection<string>

Remarks

Best-effort, like the event log it sits beside: losing a trace line must never fail the call the connector is waiting on, because a provider that sees a 500 retries the whole sync.

UnmodelledAttributes(string)

Members of a payload that Bowire does not model, in the order they appear.

public static IReadOnlyCollection<string> UnmodelledAttributes(string body)

Parameters

body string

Returns

IReadOnlyCollection<string>

Remarks

Anything outside the core User and Group attributes — the Enterprise User extension, whatever a directory maps on top. Bowire keeps them verbatim and returns them, which is right and gives no signal; this is the signal.