Skip to content

Feat: credential and auth model (tokens, proxy, delagation) - #30

Draft
Alexander-Auer wants to merge 36 commits into
DIRACGrid:mainfrom
Alexander-Auer:stack/credential-models
Draft

Alexander-Auer wants to merge 36 commits into
DIRACGrid:mainfrom
Alexander-Auer:stack/credential-models

Conversation

@Alexander-Auer

@Alexander-Auer Alexander-Auer commented Oct 20, 2026 •

Copy link
Copy Markdown

feat: credential models and provider-based supply

Part of #17.

Important

**Depends on #29 . Merge after #29 ** (PR 4 of 4; the full order is listed in #27).
Until #TESTS is merged, this diff also shows the earlier PRs. This PR's own change is one commit:
0338aa2

Implements IC-ADR-003: typed credentials, requirements computed by the backend, and supply through a provider the consumer owns.

from intercede import registry
from intercede.auth import SSHPrivateKey, StaticCredentials

async with StaticCredentials([SSHPrivateKey(pem=key_bytes)]) as credentials:
    async with registry.backend(config, credentials) as ce:
        await ce.submit(spec, copies)

intercede.auth (public)

  • Credential values: Credential, BearerToken, X509Proxy, SSHPrivateKey, SSHPassword.
    • They are immutable.
    • Secret material is SecretStr or SecretBytes, so it never appears in repr, str or JSON.
    • expires_at must carry a timezone, since it is compared with the current time.
  • CredentialSet: built as CredentialSet([token, proxy]), with at most one credential per kind. get(BearerToken) is typed as BearerToken | None. Plain dicts are refused: {"value": ...} would fit both a token and a password, and guessing the kind of a secret is never right.
  • CredentialRequirements: resource, the kinds needed together, and per-operation scopes. Only concrete credential kinds are accepted, and there must be at least one.
  • CredentialProvider: a @runtime_checkable protocol with async get(requirements), and an async context manager that the consumer opens and closes.
  • StaticCredentials: the trivial provider of ADR-003 §8 (b), for standalone use, tests and the integration stacks. Each backend receives only the kinds it asked for.

From configuration to requirements (internal)

The settings of each backend compute their requirements from the auth mode already present in BackendConfig:

Configuration Asks the provider for resource
arc / htcondor-ce, auth = ["token"] (default) BearerToken the endpoint
… auth = ["token", "proxy"] BearerToken + X509Proxy the endpoint
batch + ssh, auth = ["key"] (default) / ["password"] SSHPrivateKey / SSHPassword user@host:port
batch + ssh, auth = ["agent"], or batch + local nothing: the provider is never called n/a

As ADR-003 §2 requires, the requirements are never a public attribute of a backend.

The registry passes the provider on

  • registry.backend(config, credentials) now takes the consumer's provider. Anything that is not a provider raises ConfigurationError, and the configuration is still validated first.
  • Which parts receive it: single-class backends and transports get the provider in from_settings(settings, credentials). Schedulers do not, since they only build commands.
  • Building never asks the provider anything, since construction does no I/O.

Freshness (internal, _backends/credentials.py)

  • is_stale() is true when there is no set yet, a required kind is missing, or a required credential expires within the margin (DEFAULT_MARGIN = 5 minutes). Kinds the backend did not ask for are ignored.
  • refresh_if_stale() keeps a fresh set without calling the provider, and otherwise asks once.

Every way a provider can fail becomes an AuthenticationError, a whole-operation failure and never a per-job one:

  • the provider raises (chained to the original error);
  • it returns something other than a CredentialSet;
  • it leaves out a required kind (every missing kind is named);
  • it returns an already-expired credential.

Typed errors from the provider and task cancellation pass through unchanged.

No backend calls this yet. The first real ARC or SSH backend calls refresh_if_stale() before each operation.

Breaking change

registry.backend(config) → registry.backend(config, credentials). Nothing has been released yet.

Open points for review

  • Cloud credentials: CloudSettings has no requirements yet, because ADR-003 defines no credential type for cloud API keys.
  • A provider-raised margin: ADR-003 §3 says a provider may raise the freshness margin, but how is one of the ADR's open issues, so only the library default exists for now.
  • The SSH resource format: user@host:port was chosen so that a provider can tell several users on the same host apart.

Tests

  • tests/unit/test_auth.py: every credential kind, expiry, CredentialSet, CredentialRequirements, the provider protocol, StaticCredentials, and the public API
  • tests/unit/test_credentials.py: the margin boundaries and every provider failure
  • tests/unit/test_config.py: requirements for every auth mode
  • tests/unit/test_registry.py: the provider check, which parts receive it, and that building never asks it

643 tests pass on Python 3.11 and 3.13, with 100% line and branch coverage, and pre-commit passes.

alexander and others added 30 commits October 2, 2026 16:54
* feat: Models the core contract needs

* Feat: Protocols and Exceptions added for the core contract

* Feat: Private protocols for the BatchBackend

* feat: a fake backend to run unit tests against

* fix: some code cleanup in the helper functions

* feat: setup for the unit test suite

* chore: style fix

* chore: styling, unit test fixture

* chore: single letter var `h` refering to hanlde changed to `handle`

* fix: unused parameter in `test_injected_failure_is_reported_per_job()` removed

* feat: Models the core contract needs

* Feat: Protocols and Exceptions added for the core contract

* Feat: Private protocols for the BatchBackend

* feat: a fake backend to run unit tests against

* fix: some code cleanup in the helper functions

* feat: setup for the unit test suite

* chore: style fix

* chore: styling, unit test fixture

* chore: single letter var `h` refering to hanlde changed to `handle`

* fix: unused parameter in `test_injected_failure_is_reported_per_job()` removed

* fix: made the OutputSpec optional as it is not suppoerted by all and a consumer should choose to use it or not

---------

Co-authored-by: alexander <alexander.auer@cern.ch>
alexander and others added 6 commits October 9, 2026 16:13
Add BackendConfig, one flat public model a consumer uses to describe any
backend (kind, transport, scheduler, endpoint, host, ...). It is resolved
internally into the settings of exactly one backend family, so a field that
does not apply to the chosen kind is refused by name instead of ignored.

Add the registry: registry.backend(config) builds the backend a
configuration describes, from closed name -> "module:Class" tables. Entries
are imported lazily and every built object is checked against its protocol,
so a broken entry fails at resolution with a RegistryError.

Part of DIRACGrid#18.

Co-authored-by: alexander <alexander.auer@cern.ch>
Cover every field against every backend kind, every fake-backend operation
against every job state, and the remaining registry and model paths. Add
pytest-cov with branch coverage, measured in subprocesses too.

Co-authored-by: alexander <alexander.auer@cern.ch>
Add intercede.auth (IC-ADR-003): typed, immutable credentials with secret
fields, CredentialSet, CredentialRequirements, the CredentialProvider
protocol and a StaticCredentials provider.

Backend settings compute their requirements from the configured auth mode,
the registry takes the consumer's provider and passes it to the parts that
authenticate, and an internal freshness check asks the provider again only
when credentials are missing or about to expire. Provider failures are
whole-operation AuthenticationErrors.

registry.backend() now requires the credential provider.

Part of DIRACGrid#17.

Co-authored-by: alexander <alexander.auer@cern.ch>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant