diff --git a/CHANGELOG.D/2306.feature b/CHANGELOG.D/2306.feature new file mode 100644 index 000000000..9098f92a7 --- /dev/null +++ b/CHANGELOG.D/2306.feature @@ -0,0 +1,2 @@ +Added command `neuro blob set-bucket-publicity <"public"|"private">` to make bucket accessible for unauthorized +users. diff --git a/neuro-cli/src/neuro_cli/blob_storage.py b/neuro-cli/src/neuro_cli/blob_storage.py index 17294b6e6..eb30f78b4 100644 --- a/neuro-cli/src/neuro_cli/blob_storage.py +++ b/neuro-cli/src/neuro_cli/blob_storage.py @@ -349,7 +349,7 @@ async def statbucket( @argument("buckets", type=BUCKET, nargs=-1, required=True) async def rmbucket(root: Root, cluster: Optional[str], buckets: Sequence[str]) -> None: """ - Remove bucket DISK_ID. + Remove bucket BUCKET_ID. """ for bucket in buckets: bucket_id = await resolve_bucket( @@ -360,6 +360,37 @@ async def rmbucket(root: Root, cluster: Optional[str], buckets: Sequence[str]) - root.print(f"Bucket with id '{bucket_id}' was successfully removed.") +@command() +@option( + "--cluster", + type=CLUSTER, + help="Perform on a specified cluster (the current cluster by default).", +) +@argument("bucket", type=BUCKET, required=True) +@argument( + "public_level", + type=click.Choice(["public", "private"], case_sensitive=False), + required=True, +) +async def set_bucket_publicity( + root: Root, cluster: Optional[str], bucket: str, public_level: str +) -> None: + """ + Change public access settings for bucket BUCKET. + + Examples: + + neuro blob set-bucket-publicity my-bucket public + neuro blob set-bucket-publicity my-bucket private + """ + public = public_level == "public" + await root.client.buckets.set_public_access(bucket, public, cluster_name=cluster) + if root.verbosity >= 0: + root.print( + f"Bucket '{bucket}' was made {'public' if public else 'non-public'}." + ) + + # Object level commands @@ -958,7 +989,7 @@ async def rmcredentials( blob_storage.add_command(importbucket) blob_storage.add_command(statbucket) blob_storage.add_command(rmbucket) - +blob_storage.add_command(set_bucket_publicity) blob_storage.add_command(lscredentials) blob_storage.add_command(mkcredentials) diff --git a/neuro-cli/src/neuro_cli/formatters/buckets.py b/neuro-cli/src/neuro_cli/formatters/buckets.py index 25ae57dfd..59f29fb20 100644 --- a/neuro-cli/src/neuro_cli/formatters/buckets.py +++ b/neuro-cli/src/neuro_cli/formatters/buckets.py @@ -43,7 +43,10 @@ def _bucket_to_table_row(self, bucket: Bucket) -> Sequence[str]: self._uri_formatter(bucket.uri), ] if self._long_format: - line += [self._datetime_formatter(bucket.created_at)] + line += [ + self._datetime_formatter(bucket.created_at), + "√" if bucket.public else "×", + ] return line def __call__(self, buckets: Sequence[Bucket]) -> RenderableType: @@ -57,6 +60,7 @@ def __call__(self, buckets: Sequence[Bucket]) -> RenderableType: table.add_column("Uri") if self._long_format: table.add_column("Created at") + table.add_column("Public") for bucket in buckets: table.add_row(*self._bucket_to_table_row(bucket)) return table @@ -84,4 +88,5 @@ def __call__(self, bucket: Bucket) -> RenderableType: table.add_row("Created at", self._datetime_formatter(bucket.created_at)) table.add_row("Provider", bucket.provider) table.add_row("Imported", str(bucket.imported)) + table.add_row("Public", str(bucket.public)) return table diff --git a/neuro-cli/tests/unit/formatters/ascii/test_bucket_formatter_0.ref b/neuro-cli/tests/unit/formatters/ascii/test_bucket_formatter_0.ref index 97f6e66d7..bc231f527 100644 --- a/neuro-cli/tests/unit/formatters/ascii/test_bucket_formatter_0.ref +++ b/neuro-cli/tests/unit/formatters/ascii/test_bucket_formatter_0.ref @@ -3,4 +3,5 @@ Id bucket Name test-bucket Created at Mar 04 2017 Provider aws - Imported False + Imported False + Public False diff --git a/neuro-cli/tests/unit/formatters/ascii/test_buckets_formatter_long_0.ref b/neuro-cli/tests/unit/formatters/ascii/test_buckets_formatter_long_0.ref index 814682ead..94234dd6a 100644 --- a/neuro-cli/tests/unit/formatters/ascii/test_buckets_formatter_long_0.ref +++ b/neuro-cli/tests/unit/formatters/ascii/test_buckets_formatter_long_0.ref @@ -1,6 +1,6 @@ -Id Name Provider Uri Created at - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - bucket-1 test-bucket aws blob://cluster/user/test-bucket Mar 04 2017 - bucket-2 test-bucket-2 aws blob://cluster/user/test-bucket-2 Mar 04 2016 - bucket-3 aws blob://cluster/user-2/bucket-3 Mar 04 2018 - bucket-4 aws blob://cluster/user/bucket-4 Mar 04 2019 +Id Name Provider Uri Created at Public + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + bucket-1 test-bucket aws blob://cluster/user/test-bucket Mar 04 2017 × + bucket-2 test-bucket-2 aws blob://cluster/user/test-bucket-2 Mar 04 2016 × + bucket-3 aws blob://cluster/user-2/bucket-3 Mar 04 2018 × + bucket-4 aws blob://cluster/user/bucket-4 Mar 04 2019 √ diff --git a/neuro-cli/tests/unit/formatters/test_buckets.py b/neuro-cli/tests/unit/formatters/test_buckets.py index 8bf40ad39..d5d7f5df9 100644 --- a/neuro-cli/tests/unit/formatters/test_buckets.py +++ b/neuro-cli/tests/unit/formatters/test_buckets.py @@ -65,6 +65,7 @@ def buckets_list() -> List[Bucket]: created_at=isoparse("2019-03-04T12:28:59.759433+00:00"), provider=Bucket.Provider.AWS, imported=False, + public=True, ), ] diff --git a/neuro-sdk/docs/buckets_reference.rst b/neuro-sdk/docs/buckets_reference.rst index ecece2ac7..c3c3bfb0e 100644 --- a/neuro-sdk/docs/buckets_reference.rst +++ b/neuro-sdk/docs/buckets_reference.rst @@ -67,6 +67,18 @@ Buckets :return: Bucket credentials info (:class:`BucketCredentials`) + .. comethod:: set_public_access(bucket_id_or_name: str, public_access: bool, cluster_name: Optional[str] = None) -> Bucket + + Enable or disable public (anonymous) read access to bucket. + + :param str bucket_id_or_name: bucket's id or name. + + :param str public_access: New public access setting. + + :param str cluster_name: cluster to look for a bucket. Default is current cluster. + + :return: Bucket info (:class:`Bucket`) + .. comethod:: head_blob(bucket_id_or_name: str, key: str, cluster_name: Optional[str] = None) -> BucketEntry Look up the blob and return it's metadata. diff --git a/neuro-sdk/src/neuro_sdk/buckets.py b/neuro-sdk/src/neuro_sdk/buckets.py index 8b3152099..ae83b12fb 100644 --- a/neuro-sdk/src/neuro_sdk/buckets.py +++ b/neuro-sdk/src/neuro_sdk/buckets.py @@ -980,6 +980,7 @@ class Bucket: provider: "Bucket.Provider" created_at: datetime imported: bool + public: bool = False name: Optional[str] = None @property @@ -1024,6 +1025,7 @@ def _parse_bucket_payload(self, payload: Mapping[str, Any]) -> Bucket: created_at=isoparse(payload["created_at"]), provider=Bucket.Provider(payload["provider"]), imported=payload.get("imported", False), + public=payload.get("public", False), cluster_name=self._config.cluster_name, ) @@ -1109,6 +1111,21 @@ async def rm( async with self._core.request("DELETE", url, auth=auth): pass + async def set_public_access( + self, + bucket_id_or_name: str, + public_access: bool, + cluster_name: Optional[str] = None, + ) -> Bucket: + url = self._get_buckets_url(cluster_name) / bucket_id_or_name + auth = await self._config._api_auth() + data = { + "public": public_access, + } + async with self._core.request("PATCH", url, auth=auth, json=data) as resp: + payload = await resp.json() + return self._parse_bucket_payload(payload) + async def request_tmp_credentials( self, bucket_id_or_name: str, cluster_name: Optional[str] = None ) -> BucketCredentials: diff --git a/neuro-sdk/tests/test_buckets.py b/neuro-sdk/tests/test_buckets.py index 275d8fcb7..896720188 100644 --- a/neuro-sdk/tests/test_buckets.py +++ b/neuro-sdk/tests/test_buckets.py @@ -199,6 +199,38 @@ async def handler(request: web.Request) -> web.Response: ) +async def test_set_public( + aiohttp_server: _TestServerFactory, + make_client: _MakeClient, + cluster_config: Cluster, +) -> None: + created_at = datetime.now() + + async def handler(request: web.Request) -> web.Response: + assert request.match_info["key"] == "name" + data = await request.json() + assert data == {"public": True} + return web.json_response( + { + "id": "bucket-1", + "owner": "user", + "name": "name", + "provider": "aws", + "created_at": created_at.isoformat(), + "public": True, + } + ) + + app = web.Application() + app.router.add_patch("/buckets/buckets/{key}", handler) + + srv = await aiohttp_server(app) + + async with make_client(srv.make_url("/")) as client: + bucket = await client.buckets.set_public_access("name", True) + assert bucket.public + + async def test_rm(aiohttp_server: _TestServerFactory, make_client: _MakeClient) -> None: async def handler(request: web.Request) -> web.Response: assert request.match_info["key"] == "name"