Skip to content

feat(ol-analytics-api): deliver the MITx Online OAuth2 client credentials - #5188

Open
blarghmatey wants to merge 4 commits into
mainfrom
ol-analytics-api-mitxonline-oauth
Open

feat(ol-analytics-api): deliver the MITx Online OAuth2 client credentials#5188
blarghmatey wants to merge 4 commits into
mainfrom
ol-analytics-api-mitxonline-oauth

Conversation

@blarghmatey

@blarghmatey blarghmatey commented Jul 31, 2026

Copy link
Copy Markdown
Member

What are the relevant tickets?

Related: https://github.com/mitodl/hq/issues/10594 (this wiring is deleted once that lands)

Description (What does it do?)

Delivers the MITx Online OAuth2 client-credentials pair to the ol-analytics-api pod, so its org-manager check can authenticate with its own service token rather than forwarding the caller's identity (mitodl/ol-analytics-api#22, against the new endpoint in mitodl/mitxonline#3807).

Unlike SENTRY_DSN — which the sentry stack owns end to end — no Pulumi stack owns these values. The OAuth2 Application record is created by hand in MITx Online's Django admin.

So the vault-secrets-operator reads secret-operations directly at runtime and templates the values into the env var names the app expects, exactly as applications/marimo_data does for its OIDC client pair:

mitxonline_oauth_secrets = OLVaultK8SSecret(
    resource_config=OLVaultK8SStaticSecretConfig(
        mount="secret-operations",
        mount_type="kv-v1",
        path="ol-analytics-api/mitxonline-oauth",
        includes=["client_id", "client_secret"],
        templates={
            "OL_ANALYTICS_API_B2B_DASHBOARD_MITXONLINE_CLIENT_ID": '{{ get .Secrets "client_id" }}',
            "OL_ANALYTICS_API_B2B_DASHBOARD_MITXONLINE_CLIENT_SECRET": '{{ get .Secrets "client_secret" }}',
        },
        ...

The resulting Secret is added to the Deployment's env_from_secret_names alongside the existing static-secrets one.

The credential never passes through Pulumi state, and rotating it is just a new value in Vault — the operator picks it up on its own refresh, with no pulumi up in the loop.

One consequence worth a reviewer's attention: VSO reads that path using the application's Vault role, not the deploying operator's credentials, so ol_analytics_api_policy.hcl now grants read on it. secret-operations is kv-v1, so there's no /data/ sub-path to grant alongside.

How can this be tested?

pulumi preview on CI/QA. Expected: one new VaultStaticSecret, one new K8s Secret, an added envFrom entry on the Deployment, and the policy change. ol-analytics-api-secrets-<env> is untouched — it still carries only SENTRY_DSN.

The Vault entry must exist before the pods can start, or VSO renders empty values and the app fails at MITx Online's token endpoint:

vault kv put secret-operations/ol-analytics-api/mitxonline-oauth \
  client_id=<from MITx Online Django admin> \
  client_secret=<from MITx Online Django admin>

Pre-commit passes, including ruff and mypy. I confirmed this change adds no new mypy findings by diffing the error set against main — the file has 19 pre-existing errors, unchanged.

Not verified: no pulumi preview against the live stack (no AWS credentials in the authoring environment), and the secret-operations entry doesn't exist yet, so VSO's read and template rendering are unexercised. Both want checking before deploy.

Additional Context

Third of three PRs, and the ordering is load-bearing:

  1. feat(b2b): add a service-scoped org-manager check endpoint mitxonline#3807 — the service endpoint + b2b:manager-check scope
  2. this PR — credentials delivery
  3. fix(b2b): authenticate the org-manager check with OAuth2 client credentials ol-analytics-api#22 — the consumer

Suggested order: merge the mitxonline endpoint, create the Application and write the Vault entry, merge this, then merge the app PR.

An earlier revision of this PR read the secret with vault.generic.get_secret_output and merged it into the existing Pulumi-managed Vault path. That was wrong for the two reasons above (secret in state; rotation requires a deploy) and has been replaced with the VSO templating shown here.

Checklist:

  • Create the OAuth2 Application in MITx Online (confidential, client-credentials, b2b:manager-check scope) for each environment
  • vault kv put secret-operations/ol-analytics-api/mitxonline-oauth client_id=… client_secret=… in CI, QA, and Production Vault before applying

…ials

The app's org-manager check now authenticates to MITx Online with its own
client-credentials token instead of forwarding the caller's identity
(mitodl/ol-analytics-api#22), so the pod needs a client id and secret.

Unlike SENTRY_DSN, no Pulumi stack owns these values: the OAuth2
Application record is created by hand in MITx Online's Django admin. So
they are written once to secret-operations and read back here, then merged
into the existing kv-v2 path the vault-secrets-operator already syncs --
which means no new mount, no new K8s Secret, and no policy change, since
the app can already read that path.

Temporary. Once org-manager status is visible in the Keycloak token
(mitodl/hq#10594) the app stops calling MITx Online, and this and the Vault
entry go away together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AHYhdMkjAVj1suk9YwQzC
Copilot AI review requested due to automatic review settings July 31, 2026 17:50
Comment thread src/ol_infrastructure/applications/ol_analytics_api/__main__.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Delivers MITx Online OAuth2 client-credentials (client ID + secret) into the existing ol-analytics-api Vault static secrets path so the ol-analytics-api pod can authenticate to MITx Online using a service token for its org-manager check.

Changes:

  • Read MITx Online OAuth2 client credentials from secret-operations/ol-analytics-api/mitxonline-oauth via vault.generic.get_secret_output.
  • Merge those credentials into the existing secret-ol-analytics-api/secrets kv-v2 secret alongside SENTRY_DSN by switching to Output.all(...).apply(...).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…is wrong

The credentials at secret-operations/ol-analytics-api/mitxonline-oauth are
written by an operator, not by Pulumi, so their shape is a convention rather
than a guarantee. A typo'd key name produced a bare KeyError from inside an
apply, which is a poor way to learn that a hand-created Vault entry is
malformed.

Validate up front and name the path, the missing keys, and the command to
fix it. Empty values are rejected alongside missing ones -- reading a blank
secret would otherwise deploy an unusable credential that only fails later,
in the pod, at the token endpoint. That is why this checks explicitly rather
than switching to .get(), which would swallow the problem.

Raised by Sentry Seer on #5188.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AHYhdMkjAVj1suk9YwQzC
Reading the credential with vault.generic.get_secret_output pulled it
through Pulumi state and rewrote it into a second, Pulumi-owned Vault path.
Two problems: the secret ends up in the state file, and rotating it needs a
`pulumi up` rather than just a new value in Vault.

Have the vault-secrets-operator read secret-operations directly instead and
template the values straight into the env var names the app expects, the
same way applications/marimo_data handles its OIDC client pair. The
credential never enters Pulumi state, and a rotation propagates on the
operator's own refresh interval.

Consequence worth noting: VSO now reads that path using the application's
own Vault role, so the policy has to grant it -- added here. Previously the
read happened with the deploying operator's credentials, which is part of
why routing it through Pulumi looked deceptively simple.

This also drops the deploy-time key validation added in d566c0c for
Sentry's finding; templating happens in the operator at runtime, where
Pulumi cannot check it. Tracked in the PR discussion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AHYhdMkjAVj1suk9YwQzC
Comment thread src/ol_infrastructure/applications/ol_analytics_api/__main__.py
The previous commit added a second Secret to env_from_secret_names without
adding it to depends_on, so Pulumi was free to create the Deployment first
and the pod would crash-loop on a missing secret until the operator caught
up. static_secrets was already listed for exactly this reason; the new one
just needs to match.

Raised by Sentry Seer on #5188.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AHYhdMkjAVj1suk9YwQzC
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.

3 participants