0138

Credentials Behind an Interface

interface
secrettext
secrets
dependency-injection

Credentials Behind an Interface

An integration codeunit that reaches into a key vault — or a setup table, or Isolated Storage — on its own is a codeunit you cannot test offline and cannot repoint later. Business Central's Secrets module already publishes the abstraction to depend on instead: Interface "Secret Provider v2", implemented by the app key vault provider, by an in-memory provider, and by anything you care to write yourself. You are building the small resolver that every integration in your extension will ask for its credentials: it is handed a primary provider and a fallback provider, and it never learns where either of them keeps its secrets.

Requirements

Create a codeunit named "Credential Resolver" with three public procedures:

procedure SetProviders(NewPrimaryProvider: Interface "Secret Provider v2"; NewFallbackProvider: Interface "Secret Provider v2")
procedure Resolve(SecretName: Text): SecretText
procedure TryResolve(SecretName: Text; var SecretValue: SecretText): Boolean

Rules:

  1. SetProviders hands the resolver the two providers every later lookup must go through. Calling it a second time replaces both: the pair from an earlier call must never be consulted again. The tests always call it before asking for a secret, so you do not have to handle a resolver that was never given any providers.
  2. A lookup asks the primary provider first, and asks the fallback provider only for what the primary could not supply. Each provider is asked at most once per lookup, and when the primary answers, the fallback is not touched at all.
  3. A provider fails to supply a secret when it reports failure — and also when it reports success but hands back a secret with no content at all. Both cases move on down the chain.
  4. Resolve returns the resolved secret. When neither provider can supply it, Resolve raises an error with a message that contains the requested SecretName and no secret value of any kind.
  5. TryResolve answers the same question without ever raising an error: true with the secret in SecretValue when the chain found it, false when it did not — and on false, SecretValue comes back empty even when the caller passed in a variable that already held something.
  6. The credential must never leave the SecretText world. Interface "Secret Provider v2" declares two GetSecret overloads that differ only in the type of their var parameter, and the plain-Text one is off limits here: the providers the tests inject count every call to it, and one graded test fails unless that count is zero. That overload is also deliberately poisoned — it reports success for every name and hands back a marker value instead of a real secret — so a submission that takes the plain-Text route fails more than one test.
  7. The name you are given is the name the providers get asked for — no prefixing, no case folding, no trimming. Graded secret names are short non-empty texts, so you do not need to validate SecretName itself.

Pick object IDs in the 50100–50199 range, and reference other objects by name, never by ID.

What the tests check

The grading tests hand your resolver two task-owned Interface "Secret Provider v2" implementations that record how they were consulted, plus Microsoft's own Codeunit "In Memory Secret Provider" where the value has to come from something you cannot special-case. They resolve a randomly generated name that the primary provider holds and expect a non-empty secret back after exactly one consultation; they check that a primary hit leaves the fallback at zero consultations, that a primary miss falls through to the fallback, that the name reaches the provider character for character, and that a provider answering success-with-nothing counts as a miss so the fallback still gets its turn. Neither provider may ever see a call to the plain-Text overload of GetSecret. For a name no provider holds, asserterror expects an error whose GetLastErrorText contains that name — while a second test, with a different secret loaded into the provider, expects the message to contain neither that secret's value nor the marker value the plain-Text overload hands out. TryResolve is checked in both directions, including that a miss empties a var parameter the test deliberately passed in dirty, and a last test calls SetProviders twice and expects the first pair never to be consulted again.

Learn More

Hint 1
The resolver never picks its own providers — whatever SetProviders was handed last is what every later lookup goes through. A value of type Interface "Secret Provider v2" can live in a codeunit's var section just like a record or a codeunit can, and a local procedure can take one as a parameter, which is what turns "ask the primary, then the fallback" into the same two lines twice.
Hint 2
The interface's two GetSecret overloads differ only in the type of their var parameter, so the type of the variable you receive the value in decides which one AL calls — the plain-Text one is the trap the tests count. Note that both overloads report success as a return value, which is separate from whether anything actually came back.
Hint 3
A SecretText can be assigned from a Text variable but never read back into one, so the only question you can ask about a value you resolved is whether it has any content at all — and that is exactly the check that turns a provider's success-with-nothing into a miss. On the failing path of TryResolve, clearing the var parameter is what stops the caller's leftover value from looking like a resolved secret.
ALBusiness Central 28.5
Press Compile to check your code compiles — Submit runs the tests.
The code editor is desktop-only
Open this problem on a computer to write and run code. Reading the description, tests and discussion works fine here.