Skip to content

feat(auth): add Bitbucket authentication provider - #4629

Open
CrazyBaran wants to merge 3 commits into
github:mainfrom
CrazyBaran:feat/add-bitbucket-auth-provider
Open

CrazyBaran wants to merge 3 commits into
github:mainfrom
CrazyBaran:feat/add-bitbucket-auth-provider

Conversation

@CrazyBaran

Copy link
Copy Markdown
Contributor

Description

Why

Spec Kit's install pipeline is already host-agnostic: catalogs and archives are plain HTTPS fetches through authentication/http.py, and #2335 introduced the provider registry so credentials could be attached per host. Today that registry knows two forges — github and azure-devops. Bitbucket is the missing one.

Teams hosting internal extensions, bundles, or presets on Bitbucket Cloud or Data Center currently have two poor options:

  • Public repos work, private ones don't. There is no way to attach a Bitbucket credential, so specify extension add against a private Bitbucket catalog fails at the first 401.
  • The workaround is a misconfiguration. You can point a github provider entry at api.bitbucket.org — it "works" for Bearer access tokens because GitHubAuth is just Bearer <token> — but it is semantically wrong, invisible to future GitHub-specific logic keyed off that provider, and it cannot express Atlassian API tokens at all, which need Basic base64(<email>:<token>). The existing basic-pat scheme hardcodes an empty username (:<PAT>), so no current provider can build that header.

With Atlassian having removed app passwords on 2026-07-28, API tokens and access tokens are now the only credential types for Bitbucket Cloud, so first-class support for both is the practical minimum.

What this adds

New providersrc/specify_cli/authentication/bitbucket.py, registered as bitbucket (alphabetically, alongside the existing two):

Scheme Header Credential
bearer Authorization: Bearer <token> Cloud repository/project/workspace access tokens; Data Center HTTP access tokens
basic Authorization: Basic base64(<username>:<token>) Atlassian API tokens (username = Atlassian account email)

Config schemaAuthConfigEntry gains username: str | None = None (defaulted, appended after token_env; every construction in the repo is keyword-only, so nothing breaks). load_auth_config validates it in the existing providers[i]: … style:

  • non-empty string when present; whitespace-normalized like token_env
  • must not contain : (RFC 7617 §2 — the server splits on the first colon, so this would silently authenticate as the wrong user)
  • required when auth == "basic"; basic also joins bearer/basic-pat in requiring token or token_env

Docsdocs/reference/authentication.md: username in the fields table, a Bitbucket section with three ready-to-paste auth.json examples (Cloud access token, Atlassian API token, Data Center HTTP access token), and a note on the Downloads→S3 redirect.

Tests — 26 new cases in tests/test_authentication.py: config validation (username required/typed/normalized/colon-rejected), provider headers (including UTF-8 credentials and the bare-secret guard), token resolution, registry, and an end-to-end build_request check against api.bitbucket.org.

Design notes for reviewers

  • Composite credential instead of widening the ABC. AuthProvider.auth_headers(token, scheme) receives a single string from resolve_token(entry). For basic, BitbucketAuth.resolve_token returns "<username>:<secret>" and auth_headers encodes it verbatim. This keeps base.py, the two existing providers, and both call sites in http.py untouched. The contract is guarded: auth_headers("basic") raises if the token has no :, and resolve_token returns None (falls through to the next entry / unauthenticated) when a directly-constructed entry lacks a username — so a malformed :<secret> header can never be sent.
  • Security path unchanged. Nothing in _download_security.py, redirect validation, size limits, or sha256 verification is modified. Bitbucket Cloud's Downloads endpoint answers with a 302 to a pre-signed S3 URL; the existing _StripAuthOnRedirect drops Authorization because S3 is outside the declared hosts. That strip is required for Bitbucket (S3 returns 400 if a pre-signed request also carries an auth header), and the catalog sha256 covers integrity of the unauthenticated final hop. The docs say so explicitly.
  • UTF-8 encoding for the Basic credential, deliberately differing from azure_devops.py's ASCII encode which would raise on a non-ASCII value.
  • No URL rewriting is needed, unlike GitHub's release-asset resolver — Bitbucket's api.bitbucket.org/2.0/repositories/<ws>/<repo>/downloads/<file> accepts the token directly.

Out of scope / follow-up

An end-to-end hosting runbook (repository layout, uploading to Downloads, catalog authoring, Pipelines publish step, troubleshooting) is written and sits on a separate branch pending a decision on where it belongs in the docs; I'll open it as its own PR so this one stays reviewable as a provider change.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest — see below
  • Tested with a sample project (not applicable — no CLI surface changes; behavior is exercised by the build_request integration test)

Results on this branch (single commit on top of main @ 5e952140), Windows 11 / Python 3.14.7 / uv 0.12.14:

  • uv run pytest tests/test_authentication.py tests/test_github_http.py tests/test_download_security.py456 passed, 1 skipped (the skip is pre-existing)
  • Full uv run pytest tests/7752 passed, 461 skipped, 1 failed. The single failure is tests/test_setup_tasks.py::test_setup_tasks_ps_core_template_resolved, where Windows PowerShell 5.1 (no pwsh 7 on this machine) emits a control character in the JSON printed by scripts/powershell/setup-tasks.ps1 — environmental; scripts/powershell/ is not modified by this PR.
  • uv tool run ruff@0.15.0 check src testsAll checks passed

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

Code, tests, and documentation were generated by Claude Code (model claude-fable-5-1) working under my direction: I specified the requirement (Bitbucket Cloud/Data Center support for private extension sources), reviewed the design choice to use a composite credential rather than widen AuthProvider, and reviewed every diff before commit. The change was additionally passed through an independent AI code-review pass whose findings (the bare-secret guard, the RFC 7617 colon check, doc corrections) were verified against the codebase and Atlassian's published deprecation schedule before being applied. The commit carries the Assisted-by: trailer required by AGENTS.md.

🤖 Generated with Claude Code

Add a `bitbucket` provider to the authentication registry so private
Bitbucket Cloud and Data Center repositories can serve extension,
bundle, preset, and workflow artifacts through the existing opt-in
`~/.specify/auth.json` mechanism.

Schemes:
- `bearer` — repository/project/workspace access tokens (Cloud) and
  HTTP access tokens (Data Center): `Authorization: Bearer <token>`
- `basic` — Atlassian API tokens: `Authorization: Basic
  base64(<username>:<token>)` where username is the Atlassian account
  email. Unlike Azure DevOps' `basic-pat` (which hardcodes an empty
  username), this credential needs a real username, so `auth.json`
  gains a `username` field.

Config validation (`load_auth_config`):
- `username` must be a non-empty string when present, must not contain
  ':' (RFC 7617 §2), is whitespace-normalized like `token_env`, and is
  required when `auth == "basic"`
- `basic` joins `bearer`/`basic-pat` in requiring `token` or `token_env`

Design: `resolve_token` returns the composite `<username>:<secret>`
credential for `basic` and `auth_headers` encodes it verbatim, keeping
the `AuthProvider` interface and the shared HTTP layer unchanged.
`auth_headers("basic")` rejects a token without ':' so a bare secret
cannot silently become a well-formed header with an empty username.

No changes to the download/redirect security path. Bitbucket Cloud's
Downloads endpoint 302s to a pre-signed S3 URL; the existing redirect
handler strips `Authorization` when leaving the declared hosts, which
is required (S3 rejects pre-signed requests carrying an auth header),
and catalog `sha256` covers integrity of that hop. Documented in
docs/reference/authentication.md with ready-to-paste examples.

Tests: 26 new cases in tests/test_authentication.py covering config
validation, provider headers (incl. UTF-8 credentials and the
bare-secret guard), token resolution, registry, and an end-to-end
`build_request` check against api.bitbucket.org.

Assisted-by: Claude Code (model: claude-fable-5-1, supervised)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@CrazyBaran
CrazyBaran requested a review from mnriem as a code owner September 18, 2026 06:39
Copilot AI balanced review requested due to automatic review settings September 18, 2026 06:39

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.

🟡 Changes recommended

Directly constructed entries can bypass the colon restriction and produce an incorrectly parsed Basic credential.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds first-class Bitbucket Cloud and Data Center authentication to the provider registry.

Changes:

  • Adds Bearer and Basic authentication.
  • Validates Basic-auth usernames and credentials.
  • Documents configuration with comprehensive tests.
File summaries
File Description
src/specify_cli/authentication/bitbucket.py Implements Bitbucket authentication.
src/specify_cli/authentication/config.py Adds username validation.
src/specify_cli/authentication/__init__.py Registers the provider.
tests/test_authentication.py Tests configuration and headers.
docs/reference/authentication.md Documents Bitbucket setup.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/specify_cli/authentication/bitbucket.py
Copilot review round 1 on github#4629: load_auth_config already rejects a
'username' containing ':' (RFC 7617 §2), but resolve_token only guarded
directly-constructed entries against a missing/blank username. An entry
built in code with username "user:name" therefore resolved to
"user:name:<secret>", which a server parses as user "user". Return None
for that case as well, matching the existing missing-username defense,
and add a direct-entry regression test.

Assisted-by: Claude Code (model: claude-fable-5-1, supervised)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 18, 2026 10:24
@CrazyBaran

Copy link
Copy Markdown
Contributor Author

Review round 1 addressed in f656ee2.

BitbucketAuth.resolve_token now returns None for a basic entry whose username contains :, matching the existing missing/blank-username defense for directly constructed entries; load_auth_config already rejected this for auth.json. Added a direct-entry regression test (test_resolve_token_basic_colon_username_returns_none). Auth/HTTP/download-security suites: 457 passed, 1 pre-existing skip; ruff@0.15.0 clean.

Posted on behalf of @CrazyBaran by Claude Code (model: claude-fable-5-1, human-supervised); the fix, test, and this comment were AI-drafted and reviewed by the author before pushing.

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.

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@mnriem mnriem added the triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate label Sep 18, 2026
@mnriem
mnriem requested a balanced review from Copilot September 18, 2026 18:25

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.

🟡 Changes recommended

The Basic credential guard remains bypassable, and the dataclass field insertion breaks positional constructor compatibility.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/specify_cli/authentication/bitbucket.py Outdated
Comment thread src/specify_cli/authentication/config.py Outdated
Copilot review round 2 on github#4629:

- auth_headers("basic") checked only for the presence of ':', which still
  accepted ":<secret>" — exactly the empty-username credential the guard
  exists to block — and "<username>:". Split on the first colon with
  str.partition and require both halves to be non-empty; a secret that
  itself contains ':' is preserved intact.
- AuthConfigEntry.username was inserted before the Azure AD fields, which
  shifted positional parameter positions (a sixth positional argument
  that populated tenant_id would have become username). Append it after
  client_secret_env instead; repository call sites are keyword-only but
  the dataclass remains positionally constructible.

Tests: empty-half rejection (":secret", "user:", ":"), colon-in-secret
preservation, and a positional-construction regression for the field
order.

Assisted-by: Claude Code (model: claude-fable-5-1, supervised)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 19, 2026 10:34

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.

Copilot review overview

🟢 Approval recommended

The implementation is scoped, consistent with the provider architecture, thoroughly tested, and accurately documented.

Review effort: Balanced
Findings: None

Resolved since last review (2)

@CrazyBaran

Copy link
Copy Markdown
Contributor Author

Review round 2 addressed in ee8a99a.

  • auth_headers("basic") now splits on the first colon with str.partition and requires both halves to be non-empty, so ":<secret>" and "<username>:" are rejected alongside a bare secret; a secret containing : is preserved intact. Tests: test_basic_headers_reject_empty_half (":secret", "user:", ":") and test_basic_headers_preserve_colons_inside_secret.
  • AuthConfigEntry.username is appended after client_secret_env instead of being inserted before the Azure AD fields, so existing positional constructions keep their parameter positions. Test: test_username_field_is_appended_after_azure_fields constructs an azure-ad entry positionally and asserts tenant_id/client_id/client_secret_env land where they did before.

Auth/HTTP/download-security suites: 462 passed, 1 pre-existing skip; ruff@0.15.0 clean.

Posted on behalf of @CrazyBaran by Claude Code (model: claude-fable-5-1, human-supervised); the fixes, tests, and this comment were AI-drafted and reviewed by the author before pushing.

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

Labels

triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants