Skip to content

Commit 30def6f

Browse files
committed
Adapt multiple-bucket keys flow to authorize-account api changes
1 parent a72d314 commit 30def6f

File tree

13 files changed

+201
-112
lines changed

13 files changed

+201
-112
lines changed

b2sdk/_internal/account_info/abstract.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ class AbstractAccountInfo(metaclass=B2TraceMetaAbstract):
3131

3232
# The 'allowed' structure to use for old account info that was saved without 'allowed'.
3333
DEFAULT_ALLOWED = dict(
34-
bucketIds=None,
35-
bucketNames=None,
34+
buckets=None,
3635
capabilities=ALL_CAPABILITIES,
3736
namePrefix=None,
3837
)
@@ -337,28 +336,15 @@ def set_auth_data(
337336
@classmethod
338337
def allowed_is_valid(cls, allowed):
339338
"""
340-
Make sure that all of the required fields are present, and that
341-
bucketIds field is set if bucketNames is.
339+
Make sure that all of the required fields are present
342340
343341
If the bucketId is for a bucket that no longer exists, or the
344342
capabilities do not allow for listBuckets, then we will not have a bucketName.
345343
346344
:param dict allowed: the structure to use for old account info that was saved without 'allowed'
347345
:rtype: bool
348346
"""
349-
return (
350-
('bucketIds' in allowed)
351-
and ('bucketNames' in allowed)
352-
and (
353-
(allowed['bucketIds'] is None and allowed['bucketNames'] is None)
354-
or (
355-
allowed['bucketIds']
356-
and (len(allowed['bucketIds']) == len(allowed['bucketNames']))
357-
)
358-
)
359-
and ('capabilities' in allowed)
360-
and ('namePrefix' in allowed)
361-
)
347+
return ('buckets' in allowed) and ('capabilities' in allowed) and ('namePrefix' in allowed)
362348

363349
@abstractmethod
364350
def _set_auth_data(

b2sdk/_internal/account_info/sqlite_account_info.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,10 @@ def _migrate_allowed_to_multi_bucket(self):
408408
bucket_id = allowed.pop('bucketId')
409409
bucket_name = allowed.pop('bucketName')
410410

411-
allowed['bucketIds'] = [bucket_id] if bucket_id is not None else None
412-
allowed['bucketNames'] = [bucket_name] if bucket_name is not None else None
411+
if bucket_id is not None:
412+
allowed['buckets'] = [{'id': bucket_id, 'name': bucket_name}]
413+
else:
414+
allowed['buckets'] = None
413415

414416
allowed_text = json.dumps(allowed)
415417
stmt = f"UPDATE account SET allowed = ('{allowed_text}');"
@@ -590,8 +592,7 @@ def get_allowed(self):
590592
.. code-block:: python
591593
592594
{
593-
"bucketIds": null,
594-
"bucketNames": null,
595+
"buckets": null,
595596
"capabilities": [
596597
"listKeys",
597598
"writeKeys"

b2sdk/_internal/api.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ def check_bucket_name_restrictions(self, bucket_name: str):
636636
637637
:raises b2sdk.v2.exception.RestrictedBucket: if the account is not allowed to use this bucket
638638
"""
639-
self._check_bucket_restrictions('bucketNames', bucket_name)
639+
self._check_bucket_restrictions('name', bucket_name)
640640

641641
def check_bucket_id_restrictions(self, bucket_id: str):
642642
"""
@@ -647,14 +647,21 @@ def check_bucket_id_restrictions(self, bucket_id: str):
647647
648648
:raises b2sdk.v2.exception.RestrictedBucket: if the account is not allowed to use this bucket
649649
"""
650-
self._check_bucket_restrictions('bucketIds', bucket_id)
650+
self._check_bucket_restrictions('id', bucket_id)
651651

652652
def _check_bucket_restrictions(self, key, value):
653-
allowed = self.account_info.get_allowed()
654-
allowed_bucket_identifier = allowed[key]
653+
buckets = self.account_info.get_allowed()['buckets']
655654

656-
if allowed_bucket_identifier and value not in allowed_bucket_identifier:
657-
raise RestrictedBucket(allowed_bucket_identifier)
655+
if not buckets:
656+
return
657+
658+
for item in buckets:
659+
if item[key] == value:
660+
return
661+
662+
msg = str([b['name'] for b in buckets])
663+
664+
raise RestrictedBucket(msg)
658665

659666
def _populate_bucket_cache_from_key(self):
660667
# If the key is restricted to the bucket, pre-populate the cache with it
@@ -663,16 +670,16 @@ def _populate_bucket_cache_from_key(self):
663670
except MissingAccountData:
664671
return
665672

666-
allowed_bucket_ids = allowed.get('bucketIds')
667-
if not allowed_bucket_ids:
673+
allowed_buckets = allowed.get('buckets')
674+
if not allowed_buckets:
668675
return
669676

670-
allowed_bucket_names = allowed.get('bucketNames')
671-
672677
# If we have bucketId set we still need to check bucketName. If the bucketName is None,
673678
# it means that the bucketId belongs to a bucket that was already removed.
674-
if None in allowed_bucket_names:
675-
raise RestrictedBucketMissing
676679

677-
for _id, name in zip(allowed_bucket_ids, allowed_bucket_names):
678-
self.cache.save_bucket(self.BUCKET_CLASS(self, _id, name=name))
680+
for item in allowed_buckets:
681+
if item['name'] is None:
682+
raise RestrictedBucketMissing
683+
684+
for item in allowed_buckets:
685+
self.cache.save_bucket(self.BUCKET_CLASS(self, item['id'], name=item['name']))

b2sdk/_internal/raw_simulator.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ def __init__(
9999
key,
100100
capabilities,
101101
expiration_timestamp_or_none,
102-
bucket_ids_or_none,
103-
bucket_names_or_none,
102+
buckets_or_none,
104103
name_prefix_or_none,
105104
):
106105
self.name = name
@@ -109,15 +108,19 @@ def __init__(
109108
self.key = key
110109
self.capabilities = capabilities
111110
self.expiration_timestamp_or_none = expiration_timestamp_or_none
112-
self.bucket_ids_or_none = bucket_ids_or_none
113-
self.bucket_names_or_none = bucket_names_or_none
111+
self.buckets_or_none = buckets_or_none
114112
self.name_prefix_or_none = name_prefix_or_none
115113

114+
def _get_bucket_ids(self):
115+
if self.buckets_or_none is None:
116+
return None
117+
118+
return [item['id'] for item in self.buckets_or_none]
119+
116120
def as_key(self):
117121
return dict(
118122
accountId=self.account_id,
119-
bucketIds=self.bucket_ids_or_none,
120-
bucketNames=self.bucket_names_or_none,
123+
bucketIds=self._get_bucket_ids(),
121124
applicationKeyId=self.application_key_id,
122125
capabilities=self.capabilities,
123126
expirationTimestamp=self.expiration_timestamp_or_none
@@ -134,15 +137,15 @@ def as_created_key(self):
134137
"""
135138
result = self.as_key()
136139
result['applicationKey'] = self.key
140+
137141
return result
138142

139143
def get_allowed(self):
140144
"""
141145
Return the 'allowed' structure to include in the response from b2_authorize_account.
142146
"""
143147
return dict(
144-
bucketIds=self.bucket_ids_or_none,
145-
bucketName=self.bucket_names_or_none,
148+
buckets=self.buckets_or_none,
146149
capabilities=self.capabilities,
147150
namePrefix=self.name_prefix_or_none,
148151
)
@@ -1372,8 +1375,7 @@ def create_account(self):
13721375
key=master_key,
13731376
capabilities=ALL_CAPABILITIES,
13741377
expiration_timestamp_or_none=None,
1375-
bucket_ids_or_none=None,
1376-
bucket_names_or_none=None,
1378+
buckets_or_none=None,
13771379
name_prefix_or_none=None,
13781380
)
13791381

@@ -1401,17 +1403,6 @@ def authorize_account(self, realm_url, application_key_id, application_key):
14011403
self.auth_token_to_key[auth_token] = key_sim
14021404

14031405
allowed = key_sim.get_allowed()
1404-
bucketIds = allowed.get('bucketIds')
1405-
1406-
if bucketIds is not None:
1407-
allowed['bucketNames'] = []
1408-
for _id in bucketIds:
1409-
if _id in self.bucket_id_to_bucket:
1410-
allowed['bucketNames'].append(self.bucket_id_to_bucket[_id].bucket_name)
1411-
else:
1412-
allowed['bucketNames'].append(None)
1413-
else:
1414-
allowed['bucketNames'] = None
14151406

14161407
return dict(
14171408
accountId=key_sim.account_id,
@@ -1425,8 +1416,6 @@ def authorize_account(self, realm_url, application_key_id, application_key):
14251416
absoluteMinimumPartSize=self.MIN_PART_SIZE,
14261417
allowed=allowed,
14271418
s3ApiUrl=self.S3_API_URL,
1428-
bucketIds=allowed['bucketIds'],
1429-
bucketNames=allowed['bucketNames'],
14301419
capabilities=allowed['capabilities'],
14311420
namePrefix=allowed['namePrefix'],
14321421
),
@@ -1506,16 +1495,18 @@ def create_key(
15061495
self.app_key_counter += 1
15071496
application_key_id = 'appKeyId%d' % (index,)
15081497
app_key = 'appKey%d' % (index,)
1509-
bucket_names_or_none = None
1498+
1499+
buckets = None
1500+
15101501
if bucket_ids is not None:
15111502
# It is possible for bucketId to be filled and bucketName to be empty.
15121503
# It can happen when the bucket was deleted.
1513-
bucket_names_or_none = []
1504+
buckets = []
15141505
for _id in bucket_ids:
15151506
try:
1516-
bucket_names_or_none.append(self._get_bucket_by_id(_id).bucket_name)
1507+
buckets.append({'id': _id, 'name': self._get_bucket_by_id(_id).bucket_name})
15171508
except NonExistentBucket:
1518-
bucket_names_or_none.append(None)
1509+
buckets.append({'id': _id, 'name': None})
15191510

15201511
key_sim = KeySimulator(
15211512
account_id=account_id,
@@ -1524,8 +1515,7 @@ def create_key(
15241515
key=app_key,
15251516
capabilities=capabilities,
15261517
expiration_timestamp_or_none=expiration_timestamp_or_none,
1527-
bucket_ids_or_none=bucket_ids,
1528-
bucket_names_or_none=bucket_names_or_none,
1518+
buckets_or_none=buckets,
15291519
name_prefix_or_none=name_prefix,
15301520
)
15311521
self.key_id_to_key[application_key_id] = key_sim
@@ -2113,8 +2103,17 @@ def _assert_account_auth(
21132103
raise InvalidAuthToken('auth token expired', 'auth_token_expired')
21142104
if capability not in key_sim.capabilities:
21152105
raise Unauthorized('', 'unauthorized')
2116-
if key_sim.bucket_ids_or_none and bucket_id not in key_sim.bucket_ids_or_none:
2117-
raise Unauthorized('', 'unauthorized')
2106+
2107+
if key_sim.buckets_or_none:
2108+
found = False
2109+
for item in key_sim.buckets_or_none:
2110+
if item['id'] == bucket_id:
2111+
found = True
2112+
break
2113+
2114+
if not found:
2115+
raise Unauthorized('', 'unauthorized')
2116+
21182117
if key_sim.name_prefix_or_none is not None:
21192118
if file_name is not None and not file_name.startswith(key_sim.name_prefix_or_none):
21202119
raise Unauthorized('', 'unauthorized')

b2sdk/_internal/session.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,7 @@ def _construct_allowed_dict(self, storage_api_info):
143143
# `allowed` object has been deprecated in the v3 of the API, but we still
144144
# construct it artificially to avoid changes in all the reliant parts.
145145

146-
return {
147-
'bucketIds': storage_api_info['bucketIds'],
148-
'bucketNames': storage_api_info['bucketNames'],
149-
'capabilities': storage_api_info['capabilities'],
150-
'namePrefix': storage_api_info['namePrefix'],
151-
}
146+
return storage_api_info['allowed']
152147

153148
def cancel_large_file(self, file_id):
154149
return self._wrap_default_token(self.raw_api.cancel_large_file, file_id)
@@ -510,15 +505,17 @@ def _add_app_key_info_to_unauthorized(self, unauthorized):
510505
# What's allowed?
511506
allowed = self.account_info.get_allowed()
512507
capabilities = allowed['capabilities']
513-
bucket_name = allowed['bucketName']
514508
name_prefix = allowed['namePrefix']
515509

516510
# Make a list of messages about the application key restrictions
517511
key_messages = []
518512
if set(capabilities) != set(ALL_CAPABILITIES):
519513
key_messages.append("with capabilities '" + ','.join(capabilities) + "'")
520-
if bucket_name is not None:
521-
key_messages.append("restricted to bucket '" + bucket_name + "'")
514+
515+
allowed_buckets_msg = self._get_allowed_buckets_message(allowed)
516+
if allowed_buckets_msg:
517+
key_messages.append(allowed_buckets_msg)
518+
522519
if name_prefix is not None:
523520
key_messages.append("restricted to files that start with '" + name_prefix + "'")
524521
if not key_messages:
@@ -530,6 +527,15 @@ def _add_app_key_info_to_unauthorized(self, unauthorized):
530527

531528
return Unauthorized(new_message, unauthorized.code)
532529

530+
def _get_allowed_buckets_message(self, allowed) -> str | None:
531+
buckets = allowed['buckets']
532+
if not buckets:
533+
return None
534+
535+
bucket_names = [b['name'] for b in buckets]
536+
537+
return f'restricted to buckets {bucket_names}'
538+
533539
def _get_upload_data(self, bucket_id):
534540
"""
535541
Take ownership of an upload URL / auth token for the bucket and

b2sdk/v2/account_info.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,15 @@ def get_allowed(self):
7777

7878
# convert a multi-bucket key to a single bucket
7979

80-
if 'bucketIds' in allowed:
81-
bucket_ids = allowed.pop('bucketIds')
82-
if bucket_ids and len(bucket_ids) > 1:
80+
if 'buckets' in allowed:
81+
buckets = allowed.pop('buckets')
82+
if buckets and len(buckets) > 1:
8383
raise MissingAccountData(
8484
'Multi-bucket keys cannot be used with the current sdk version'
8585
)
8686

87-
allowed['bucketId'] = bucket_ids[0] if bucket_ids else None
88-
89-
bucket_names = allowed.pop('bucketNames')
90-
allowed['bucketName'] = bucket_names[0] if bucket_names else None
87+
allowed['bucketId'] = buckets[0]['id'] if buckets else None
88+
allowed['bucketName'] = buckets[0]['name'] if buckets else None
9189

9290
return allowed
9391

b2sdk/v2/session.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,10 @@ def _construct_allowed_dict(self, storage_api_info):
8989
'capabilities': storage_api_info['capabilities'],
9090
'namePrefix': storage_api_info['namePrefix'],
9191
}
92+
93+
def _get_allowed_buckets_message(self, allowed) -> str | None:
94+
bucket_name = allowed['bucketName']
95+
if bucket_name is None:
96+
return None
97+
98+
return "restricted to bucket '" + bucket_name + "'"

test/unit/account_info/test_account_info.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,7 @@ def allowed(self, apiver):
197197
return dict(
198198
capabilities=['readFiles'],
199199
namePrefix=None,
200-
bucketIds=None,
201-
bucketNames=None,
200+
buckets=None,
202201
)
203202

204203
def test_set_auth_data_compatibility(self, account_info_default_data, allowed):

test/unit/account_info/test_sqlite_account_info.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ def test_migrate_to_5_v2(
106106
namePrefix=None,
107107
),
108108
dict(
109-
bucketIds=['123'],
110-
bucketNames=['bucket1'],
109+
buckets=[{'id': '123', 'name': 'bucket1'}],
111110
capabilities=['listBuckets', 'readBuckets'],
112111
namePrefix=None,
113112
),
@@ -120,8 +119,7 @@ def test_migrate_to_5_v2(
120119
namePrefix=None,
121120
),
122121
dict(
123-
bucketIds=None,
124-
bucketNames=None,
122+
buckets=None,
125123
capabilities=['listBuckets', 'readBuckets'],
126124
namePrefix=None,
127125
),
@@ -153,8 +151,7 @@ def test_multi_bucket_key_error_apiver_v2(
153151
account_info_default_data,
154152
):
155153
allowed = dict(
156-
bucketIds=[1, 2],
157-
bucketNames=['bucket1', 'bucket2'],
154+
buckets=[{'id': 1, 'name': 'bucket1'}, {'id': 2, 'name': 'bucket2'}],
158155
capabilities=['listBuckets', 'readBuckets'],
159156
namePrefix=None,
160157
)

0 commit comments

Comments
 (0)