Skip to content

feat: add Personal Access Token authentication with project scoping - #41

Open
zajca wants to merge 2 commits into
mainfrom
feat/pat-auth-project-scoping
Open

feat: add Personal Access Token authentication with project scoping#41
zajca wants to merge 2 commits into
mainfrom
feat/pat-auth-project-scoping

Conversation

@zajca

@zajca zajca commented Jul 30, 2026

Copy link
Copy Markdown
Member

What changed

Both subprojects accepted only a project-scoped Storage API token (X-StorageApi-Token). They now also accept a user-scoped Personal Access Token, which Connection authenticates as a bearer credential and scopes per request via X-KBC-ProjectId.

The credential kind is detected from the kbc_pat_ prefix, so existing connections keep working with no configuration change. The JDBC driver additionally takes an explicit auth=token|pat override.

This is the first of two PRs. The second adds programmatic sessions (PKCE + device code) on top of the same abstraction.

Observable difference

Before After
Only a Storage API token; one token per project Also a PAT: one credential across every project you can reach
project property (JDBC) / Project ID field (VSCode), auto-detected when the token reaches exactly one project
DBeaver/DataGrip offer reachable projects as a drop-down; VSCode offers a QuickPick
workspace advertised as required advertised as optional, matching the existing auto-select of the newest workspace

Server-side facts this relies on

Verified against keboola/connection, not assumed:

  • A PAT is sent as Authorization: Bearer; every Storage API path additionally requires X-KBC-ProjectId (BearerTokenAuthenticator). A project outside scope is 403.
  • GET /v1/auth/pat returns {items: [...]}, each item carrying a live resolved projects array — populated even for an {"all": true} scope. It is a /v1/auth/* route, not a storage route, so it must not carry the project header.
  • With a PAT bearer the listing covers the calling token and its descendants. A derived token can never exceed its parent, so the union of projects across items is exactly the caller's own access set. The driver never needs to identify which item is "self".
  • Programmatic auth is a gated stack feature: when disabled, /v1/auth/* returns 404, not 403. Both clients translate that into "not enabled on this stack" rather than a generic HTTP error.
  • GET /v1/auth/token/introspect is session-only and returns 403 for a PAT bearer, which is why PAT metadata comes from /v1/auth/pat.

Design notes

AuthProvider.authHeaders() is resolved per request, and re-resolved on every retry attempt, rather than captured in a constructor. Nothing in this PR needs that — a PAT does not expire mid-connection — but it is the seam the next PR's refreshable session credential plugs into without touching the HTTP clients.

Host URL normalization moved to HostUrls so ProgrammaticAuthClient shares it with StorageApiClient. Behaviour is unchanged and was verified by diffing executable statements against the original, not by eye. It is a security control: plaintext http:// is honored for loopback only, and any remote http:// base is upgraded to https:// because a credential rides on every request. HostUrlsTest covers it directly, including fail-closed cases such as http://localhost.evil.example.com, http://localhost@evil.example.com and http://evil.example.com/localhost, all of which upgrade.

Project ids are long/string everywhere. They can exceed Integer.MAX_VALUE, and the VSCode side keeps them strings end to end to stay clear of the JS safe integer range.

Verification

  • JDBC: 526 unit tests, 0 failures, 0 errors, BUILD SUCCESS. Run in the maven:3.9-eclipse-temurin-21 image against the module source, because Maven is not installed on the machine this was developed on.
  • VSCode: builds and typechecks. npm ci, npm run compile and tsc -p tsconfig.test.json all succeed, and tsc --noEmit reports no new errors versus main (the 19 pre-existing ones in ls/queries.ts and ls/driver.ts are unchanged; tsup does not typecheck, which is why they exist on main).
  • The extension test suite was not executed locallyvscode-test could not download VS Code in this environment. CI runs xvfb-run -a npm test, so this PR's CI run is the first runtime gate for the 206 extension tests. Please confirm it is green before merging.

Follow-ups, deliberately not in this PR

  • Bare http://::1 without a port is not recognized as loopback and gets upgraded, because the port split at the last colon leaves ":". It fails closed, so it is not a hole, and it predates this PR; http://[::1] is the reliable spelling. HostUrlsTest asserts both.
  • readOnly: true on a PAT is rejected at issuance by Connection until runtime enforcement ships, so the clients do not surface it.

zajca added 2 commits July 30, 2026 15:53
Both the JDBC driver and the VSCode extension accepted only a project-scoped
Storage API token sent in X-StorageApi-Token. They now also accept a
user-scoped Personal Access Token, which Connection authenticates as a bearer
credential and scopes per request via X-KBC-ProjectId.

The credential kind is detected from the kbc_pat_ prefix, so existing
connections keep working with no configuration change. The JDBC driver also
takes an explicit `auth=token|pat` override.

JDBC driver:
- New com.keboola.jdbc.auth package. AuthProvider.authHeaders() is resolved per
  request (and re-resolved on every retry attempt) rather than captured in the
  constructor, so a credential that expires can be renewed without touching the
  HTTP clients. StorageTokenAuthProvider and PatAuthProvider are immutable and
  redact the credential in toString().
- All three HTTP clients take an AuthProvider instead of a token String;
  StorageApiClient.getToken() is replaced by getAuthProvider().
- New ProgrammaticAuthClient for GET /v1/auth/pat, which is not a storage route
  and therefore carries the bearer only. HTTP 404 there means programmatic auth
  is disabled on the stack, 401 means the token is invalid.
- New `project` property. When absent for a PAT, the driver takes the union of
  the accessible projects across the returned items -- a derived token can never
  exceed its parent, so that union is the caller's own access set. One project
  auto-selects, several fail listing them, none fails as an auth error.
- getPropertyInfo exposes `auth` and `project`, populating project choices from
  the PAT. workspace is no longer advertised as required, matching
  resolveWorkspaceId auto-selecting the newest workspace.
- Host URL normalization moved to HostUrls so ProgrammaticAuthClient shares it.
  Behaviour is unchanged: plaintext http:// is honored for loopback only, any
  remote http:// base is upgraded to https:// because a credential rides on
  every request.

VSCode extension:
- New src/auth.ts owns credential detection, header construction and project
  discovery, shared by the extension host and the language server so both
  report the same diagnosis.
- New optional projectId connection field, selected via QuickPick in
  parseBeforeSaveConnection when a PAT reaches more than one project.
- Project ids stay strings end to end; they can exceed the safe integer range.

Verified: 526 JDBC unit tests pass. The extension builds and its sources and
test sources typecheck; its test suite could not be executed locally because
vscode-test cannot download VS Code in this environment, so CI is the first
runtime gate for it.
KeboolaDriverIT builds its connection properties from the environment in two
places. Both now go through one helper that also honours KEBOOLA_PROJECT, so the
suite can run against a Personal Access Token that reaches several projects.
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