Skip to content

docs: B2B learner records spec and provider-access decision - #55

Open
blarghmatey wants to merge 6 commits into
mainfrom
docs/b2b-learner-records-spec
Open

docs: B2B learner records spec and provider-access decision#55
blarghmatey wants to merge 6 commits into
mainfrom
docs/b2b-learner-records-spec

Conversation

@blarghmatey

@blarghmatey blarghmatey commented Sep 10, 2026

Copy link
Copy Markdown
Member

What are the relevant tickets?

N/A

Description (What does it do?)

Commits the B2B learner records spec, which until now existed only as untracked local files, and records the decision on its blocking open question: who authorizes a training provider to read an organization's learner data.

  • b2b-learner-records-onepager.md: high-level design and open questions.
  • b2b-learner-records-design.md: data backing, tenancy rationale, scope, consent.
  • openapi/b2b-learner-records-v1.yaml: draft OpenAPI 3.1 contract.
  • b2b-learner-records-provider-authorization.md (new): the access decision.
    • Access is settled when the contract is signed. MIT doesn't mediate it at request time, and the partner handles per-user authorization in its own LMS.
    • One Keycloak client per contracted integration, in Pulumi. The client carries its organization UUIDs as a hardcoded claim (HardcodedClaimProtocolMapper) and the single learner-records:read scope.
    • The API checks the path's organization against the claim. No grant store, and no call to mitxonline.
    • Identity is not redacted: the organization already holds its learners' identity. Records and exports always carry email/full_name, and the draft's read-pii scope is dropped.
    • Recommendations not yet applied to the contract: a contract-end-date claim the API enforces, and presigned export URLs so the API client is the only credential.

Changes to the draft OpenAPI:

  • contract_id is integer (int64) everywhere. It's mitxonline's ContractPage id (dim_contract.b2b_contract_id, BIGINT in the production warehouse), the same identifier fix(b2b_dashboard): type contract_id as the integer it actually is #39 retyped in b2b_dashboard.
  • Enrollment grain is (learner, contract, course run), matching the backing model. No organization/course-run pair in production links to more than one contract today (0 of 2468).
  • Every list endpoint has a unique order for stable offset paging, and /learners?contract_id= recomputes rollups within that contract.
  • Fixed-shape nullable fields are required.
  • The security scheme says one client per contracted integration, and the markdown code fences that made the file unparseable are gone.

How can this be tested?

Docs only. openapi-spec-validator docs/openapi/b2b-learner-records-v1.yaml reports OK, and the repo's check-yaml hook passes. The code references in the decision doc were grepped against current ol-infrastructure and ol-analytics-api. The design doc's warehouse claims and the contract figures above were checked against the ol-data-platform dbt models and the production warehouse through local DuckDB. HardcodedClaimProtocolMapper was confirmed present in pulumi-keycloak 6.13.0, within ol-infrastructure's >=6.0.0,<7 pin.

Additional Context

Three things in the decision doc to verify on QA before building: which claim carries the client ID in a Keycloak 26.7 client-credentials token, whether APISIX passes the hardcoded claim and scope through X-Userinfo on a bearer-only route, and the access-token lifespan (the revocation window).

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBTqmr3ki2pAAP1EeWH7wg

blarghmatey and others added 2 commits September 10, 2026 14:45
The learner records design had only ever existed as untracked files, so
nobody could review it or link to it. This commits the one-pager, the
data-backing/tenancy doc and the draft OpenAPI contract as they stood,
plus a proposal for the blocking open question: who authorizes a
training provider to read an organization's learner data.

The proposal separates the provider's credential (a Keycloak client,
Pulumi-owned per the B2B onboarding substrate/tenant split) from the
per-organization grant (a mitxonline record). The organization
authorizes, MIT records and enforces it, and phase 1 lets the
organization see and revoke grants before self-service authorization
exists. It is marked pending sign-off. Nothing is implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBTqmr3ki2pAAP1EeWH7wg
Access to the M2M API is settled when the contract is signed, not
mediated by mitxonline at request time, and the partner handles
per-user authorization in its own LMS. That replaces the grant-record
proposal: the per-contract Keycloak client is the whole record, with
its organizations as a hardcoded claim and identity as the read-pii
scope.

The OpenAPI file was wrapped in markdown code fences, so it was not
parseable YAML as committed. Stripping them makes it pass check-yaml
and openapi-spec-validator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBTqmr3ki2pAAP1EeWH7wg
@blarghmatey blarghmatey changed the title docs: B2B learner records spec and provider-authorization proposal docs: B2B learner records spec and provider-access decision Sep 10, 2026
@blarghmatey
blarghmatey marked this pull request as ready for review September 10, 2026 19:09
blarghmatey and others added 2 commits September 10, 2026 15:49
…gn facts

contract_id is mitxonline's ContractPage id, exposed by dim_contract as
b2b_contract_id: an integer. The spec typed it as a string with a UUID
example, the same mismatch #39 fixed in b2b_dashboard after production
rejected input_value=695. A partner generating a client from this
contract would have expected UUID strings.

Also corrects three stale statements in the design doc: §4 still titled
the provider-authorization question "Open" above its decision, the
b2b_analytics MV count is eight rather than six, and the roster's
manager column is userorganization_is_manager.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBTqmr3ki2pAAP1EeWH7wg
dim_contract.contract_id and the mitxonline source column
b2b_contractpage.page_ptr_id are both BIGINT in the production
warehouse, so the contract states the width a partner's client should
use rather than leaving it to generator defaults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBTqmr3ki2pAAP1EeWH7wg

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The export design can bypass PII scope restrictions, and several contract semantics remain inconsistent or ambiguous.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Documents the proposed B2B learner-records API and its provider-authorization model.

Changes:

  • Adds the draft OpenAPI contract and data design.
  • Defines consent, tenancy, and export behavior.
  • Records contract-based Keycloak client authorization.
File summaries
File Description
docs/openapi/b2b-learner-records-v1.yaml Defines the draft API contract.
docs/b2b-learner-records-provider-authorization.md Records the provider-access decision.
docs/b2b-learner-records-onepager.md Summarizes scope and open questions.
docs/b2b-learner-records-design.md Describes data backing and tenancy.
Review details

Suppressed comments (1)

docs/openapi/b2b-learner-records-v1.yaml:549

  • The prose promises that PII fields return null without the PII scope and that every field after outcomes_shared is nulled when outcomes are withheld, but the required list permits all of those fields to be omitted. Require the stable record fields and use null only as the documented unavailable/withheld value.
      required:
  • Files reviewed: 4/4 changed files
  • Comments generated: 9
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/openapi/b2b-learner-records-v1.yaml Outdated
Comment thread docs/b2b-learner-records-design.md Outdated
Comment thread docs/openapi/b2b-learner-records-v1.yaml Outdated
Comment thread docs/openapi/b2b-learner-records-v1.yaml Outdated
Comment thread docs/openapi/b2b-learner-records-v1.yaml
Comment thread docs/openapi/b2b-learner-records-v1.yaml
Comment thread docs/openapi/b2b-learner-records-v1.yaml
Comment thread docs/openapi/b2b-learner-records-v1.yaml
Comment thread docs/openapi/b2b-learner-records-v1.yaml Outdated
blarghmatey and others added 2 commits September 10, 2026 16:39
Drops the read/read-pii scope split. The organization already holds its
learners' identity because they are its employees or students, so
redacting it protects nothing. A single per-organization export could
not have honoured the split anyway. Records and exports always carry
identity, and one scope remains.

Pins down contract semantics the review found ambiguous:
- Enrollment grain includes the contract, matching the backing model.
  No org/run pair in production links to more than one contract today
  (0 of 2468), so this adds no duplicates.
- Every list endpoint has a unique order, so offset paging is stable.
- /learners?contract_id= recomputes rollups from that contract's
  enrollments only.
- Fixed-shape nullable fields are required, so omission is not a
  second way to say null.
- The x-data-readiness claim is limited to the record schemas, and the
  security scheme says "per contracted integration", matching the
  decision doc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBTqmr3ki2pAAP1EeWH7wg
… scope

The organization can see which learners declined by diffing its roster,
and no response shape changes that. How it may use that knowledge is a
contract term owned by legal and contracting, and issuing a client
presumes those terms exist, so the spec drops the options list and keeps
the per-learner record shape.

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

2 participants