Skip to content

Support v4 Multi-Bucket Application Keys - #30

Open
johnmaguire wants to merge 10 commits into
Backblaze:masterfrom
johnmaguire:support-v4-api
Open

Support v4 Multi-Bucket Application Keys#30
johnmaguire wants to merge 10 commits into
Backblaze:masterfrom
johnmaguire:support-v4-api

Conversation

@johnmaguire

@johnmaguire johnmaguire commented Apr 15, 2026

Copy link
Copy Markdown

Fixes #27 with the ultimate goal of solving restic/restic#5741

A new b2.CreateKeyMultiBucket was added to avoid breaking the type signature of b2.CreateKey, with support for multi-bucket application keys.

b2.CreateKey continues to use the old API (similar to the terraform provider's usage of bucket_id vs bucket_ids) in order to support clients which do not support multi-bucket application keys.

Client.CreateKey accepts a new BucketIDs option and selects CreateKeyMultiBucket when len(bucketIDs) > 1.

References:

@mlech-reef mlech-reef self-assigned this May 18, 2026

@mlech-reef mlech-reef 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.

v4 b2_authorize_account response is parsed from the wrong location - restricted keys lose their scope

The v4 auth response nests key restrictions under apiInfo.storageApi.allowed:

  • storageApi.allowed.buckets[]{ id, name }
  • storageApi.allowed.namePrefix
  • storageApi.allowed.capabilities

But StorageAPIInfo reads bucketIds/bucketNames/namePrefix at the storageApi top level, where v4 emits nothing. So for any bucket- or prefix-restricted key, B2.buckets and B2.pfx come back empty. The client never learns its own restriction, lists outside the allowed scope, and B2 rejects with 401:

=== RUN   TestListBucketContentsWithKey
    integration_test.go:1079: b2_list_file_names: 401:
--- FAIL: TestListBucketContentsWithKey (6.24s)

This also breaks the headline feature: a multi-bucket key authorizes but the client forgets which buckets it's scoped to.

Master (v3) returned bucketId/bucketName at the top level, so this only surfaced with the v4 migration. Unrestricted/master keys are unaffected (their allowed is empty), which is why most tests pass.

@mlech-reef

Copy link
Copy Markdown
Collaborator

Adds a live integration test that mints a Multi-Bucket Application Key and
confirms it can list within each allowed bucket.
@johnmaguire

johnmaguire commented Jun 26, 2026

Copy link
Copy Markdown
Author

@mlech-reef Thanks for taking a look, and apologies on the miss - added an integration test for a multi-bucket key this time around and ran the suite.

@johnmaguire
johnmaguire requested a review from mlech-reef July 22, 2026 08:07
@mlech-reef

Copy link
Copy Markdown
Collaborator

Hi @johnmaguire. Sorry for the delay, but my capacity for this project is very low right now. I can confirm that you fix has solved my previous claim. For the rest, I did some LLM-assisted review and I found few things which we need to address. Please check them

@mlech-reef mlech-reef 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.

Verified against the live B2 v3/v4 APIs (master key, single-bucket key, multi-bucket key over two buckets, prefix-only key), the full live integration suite, and go vet/gofmt — details inline.

First, the good news: the v4 struct shapes match the real wire format exactly (apiInfo.storageApi.allowed.buckets[].{id,name}, allowed.namePrefix, bucketIds in create-key request/response); keeping CreateKey on v3 is correct and necessary (v4 b2_create_key rejects singular bucketId: "The bucketId parameter has been deprecated in favor of bucketIds list"); v4 b2_delete_key/b2_list_keys interop correctly with v3-created keys; and the "multi-bucket keys are unusable by v3-only clients" claim is accurate (v3 auth rejects them with 400).

Blocking

  1. ListBuckets() 401s for multi-bucket keysb2_list_buckets does no server-side scope enforcement; without a bucketId filter it rejects any bucket-restricted key. A multi-bucket-key client can look up buckets by name but cannot enumerate them. Needs a per-bucket fan-out or an explicit error.

  2. Dispatch threshold contradicts the PR body — body says len(bucketIDs) > 1, code does > 0, so BucketIDs("one-id") mints a v4-only key that v3 clients can't use at all. That drops v3 compat in exactly the single-bucket case this PR's v3-compat design is meant to preserve. Fixing it also incidentally fixes #1 for single-ID keys, but ≥2-ID keys still hit #1, so both changes are needed.

Nits

  1. Live v4 returns allowed as an object with null fields for master keys ({"buckets": null, ..., "namePrefix": null}), not an absent field — the code handles it, but the comments and the v4AuthJSON fixture encode the wrong shape, so nothing pins the real master-key format.
  2. AllowedBucket.Name is returned even without listBuckets (verified live); it's only empty for deleted buckets.
  3. DefaultUserAgent is still blazer/0.7.2.
  4. Key.BucketID/Key.BucketIDs are parsed but never surfaced.
  5. Pre-existing, slightly more relevant now: B2.Update() doesn't copy buckets/pfx on re-auth.
  6. Pre-existing: Client.CreateKey rejects account-wide prefix-only keys even though B2 accepts them and the new parsing handles them; the new error message reads like a B2 constraint rather than a library one.

Comment thread base/base.go
Comment thread b2/key.go Outdated
Comment thread b2/b2_test.go
Comment thread base/base.go Outdated
Comment thread base/base_v4_test.go Outdated
Comment thread internal/b2types/b2types.go Outdated
Comment thread base/base.go
A v4-minted key cannot authorize against v3, so minting single-bucket
keys via v4 silently broke them for v3-only clients. Dispatch to
createKeyMultiBucket only for two or more bucket IDs, matching the
documented behavior that CreateKey produces legacy single-bucket keys.
b2_list_buckets does no scope narrowing for restricted keys: it rejects
any unfiltered request with 401 and accepts at most one bucketId filter.
A key restricted to two or more buckets therefore could never enumerate
them. Issue one filtered request per allowed bucket and merge, so
ListBuckets returns the key's visible buckets as it does for
single-bucket keys.
Live v4 returns allowed as an object with null buckets and namePrefix
for master keys, not an absent field. The parsing already handled both,
but the fixture and comments encoded the absent-field shape, so nothing
verified the format B2 actually emits.
The name is returned regardless of the listBuckets capability; it is
empty only when the bucket has been deleted.
Update dropped buckets and pfx on re-auth, silently widening the
client's view of its own restrictions. Benign while re-auth reuses the
same credentials, but a latent trap now that scope drives ListBuckets
filtering.
The v4 migration and Multi-Bucket Application Key support are a minor
version under semver.
TestListBucketsWithKey and TestListBucketContentsWithKey left their
application key behind on every live run; every other key-minting live
test already cleans up after itself.
@johnmaguire

Copy link
Copy Markdown
Author

Hi @mlech-reef - no worries, I completely understand.

I believe I have addressed all the review feedback, and ran a passing live test against my account.

@johnmaguire
johnmaguire requested a review from mlech-reef August 31, 2026 19:15
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.

Support Native API v4

2 participants