Skip to content

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

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

CrazyBaran wants to merge 4 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.

mnriem
mnriem previously approved these changes Sep 22, 2026

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 consistent with the provider architecture, documented, and adequately covered by positive and negative tests.

Review effort: Balanced
Findings: None

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 consistent with the provider architecture and includes thorough positive and negative coverage.

Review effort: Balanced
Findings: None

@CrazyBaran
CrazyBaran requested review from mnriem and a balanced review from Copilot September 23, 2026 06:14

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 focused, preserves existing contracts, and includes strong positive and negative coverage.

Review effort: Balanced
Findings: None

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

🟡 Changes recommended

The Cloud examples declare bitbucket.org despite requiring host-specific authentication credentials not represented by those entries.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Low severity

Open (1)


```json
{
"hosts": ["api.bitbucket.org", "bitbucket.org"],

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address Copilot feedback

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

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