From 92b5a3e2cfdc9dad2b1be894b9e22a8fe686c990 Mon Sep 17 00:00:00 2001 From: Matthew Aitken Date: Mon, 17 Mar 2025 10:55:16 -0700 Subject: [PATCH 1/3] adds aws_sigv4 signer for s3 requests, adds python s3 credentials --- package-lock.json | 243 +++++++++++++++++- package.json | 3 + pyproject.toml | 2 + python/neuroglancer/aws_credentials.py | 69 +++++ .../default_credentials_manager.py | 6 + python/neuroglancer/server.py | 27 ++ src/credentials_provider/aws_sigv4.ts | 126 +++++++++ src/credentials_provider/http_request.ts | 16 +- src/credentials_provider/oauth2.ts | 4 +- src/datasource/boss/api.ts | 2 +- src/datasource/dvid/api.ts | 2 +- src/datasource/nggraph/frontend.ts | 2 +- src/kvstore/gcs/register.ts | 17 +- src/kvstore/ngauth/credentials_provider.ts | 2 +- src/kvstore/s3/common.ts | 22 +- src/kvstore/s3/index.rst | 23 +- .../credentials_provider.ts | 14 + uv.lock | 109 ++++++++ 18 files changed, 671 insertions(+), 18 deletions(-) create mode 100644 python/neuroglancer/aws_credentials.py create mode 100644 src/credentials_provider/aws_sigv4.ts diff --git a/package-lock.json b/package-lock.json index fba2dc2571..09994be98b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "2.40.1", "license": "Apache-2.0", "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@smithy/signature-v4": "^5.0.1", "codemirror": "^5.61.1", "core-js": "^3.40.0", "crc-32": "^1.2.2", @@ -18,6 +20,7 @@ "msgpackr": "^1.11.2", "nifti-reader-js": "^0.6.8", "numcodecs": "^0.3.2", + "smithy": "^0.6.3", "valibot": "^1.0.0-beta.15" }, "devDependencies": { @@ -99,6 +102,44 @@ "dev": true, "license": "ISC" }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", + "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -2457,6 +2498,163 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz", + "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz", + "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz", + "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz", + "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -4810,6 +5008,37 @@ "node": ">=6" } }, + "node_modules/canihaz": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/canihaz/-/canihaz-1.1.0.tgz", + "integrity": "sha512-9kSVfZYHDQiLbx6FkFf9/gWviCCr3YSE5BxZ2cVDw7osAHGRnL4p/Tx6Lwmgz0+lKBqTyc+nKx6+kOMWCfUmSw==", + "dependencies": { + "mkdirp": "0.5.x", + "semver": "5.5.x", + "which": "1.3.x" + } + }, + "node_modules/canihaz/node_modules/semver": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/canihaz/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001695", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", @@ -10057,7 +10286,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10074,7 +10302,6 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, "dependencies": { "minimist": "^1.2.6" }, @@ -12393,6 +12620,15 @@ "npm": ">= 3.0.0" } }, + "node_modules/smithy": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/smithy/-/smithy-0.6.3.tgz", + "integrity": "sha512-8OeoWitzgr7cwMOSUqAbMlBuep/iTg3pzOemdN2x7HhhaYo66SANRumlaWEhSPObXF/L4HMkGTXLq1ZNUU9nDw==", + "license": "MIT", + "dependencies": { + "canihaz": "1.1.x" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -13246,8 +13482,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsx": { "version": "4.19.2", diff --git a/package.json b/package.json index 333f1add2e..cb4db60c69 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,8 @@ "yauzl": "^3.2.0" }, "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@smithy/signature-v4": "^5.0.1", "codemirror": "^5.61.1", "core-js": "^3.40.0", "crc-32": "^1.2.2", @@ -95,6 +97,7 @@ "msgpackr": "^1.11.2", "nifti-reader-js": "^0.6.8", "numcodecs": "^0.3.2", + "smithy": "^0.6.3", "valibot": "^1.0.0-beta.15" }, "overrides": { diff --git a/pyproject.toml b/pyproject.toml index 7d9e51a129..083cfd6b96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "atomicwrites>=1.4.1", "google-apitools>=0.5.32", "google-auth>=2.38.0", + "boto3>=1.31.16", "numpy>=1.11.0", "pillow>=3.2.0", "requests>=2.32.3", @@ -109,6 +110,7 @@ mypy = [ "mypy>=1.14.1", "pandas-stubs>=2.2.3.241126", "types-atomicwrites>=1.4.5.1", + "types-boto3>=1.31.16", "types-pillow>=10.2.0.20240822", "types-requests>=2.32.0.20241016", "types-setuptools>=75.8.0.20250110", diff --git a/python/neuroglancer/aws_credentials.py b/python/neuroglancer/aws_credentials.py new file mode 100644 index 0000000000..d49870d4a3 --- /dev/null +++ b/python/neuroglancer/aws_credentials.py @@ -0,0 +1,69 @@ +# @license +# Copyright 2025 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging +import threading + +from . import credentials_provider +from .futures import run_on_new_thread + + +class AWSApplicationDefaultCredentialsProvider( + credentials_provider.CredentialsProvider +): + def __init__(self): + super().__init__() + + # Make sure logging is initialized. Does nothing if logging has already + # been initialized. + logging.basicConfig() + + self._lock = threading.Lock() + self._credentials = None + + def get_new(self): + def func(): + with self._lock: + if self._credentials is None: + import boto3 + + session = boto3.session.Session() + credentials = session.get_credentials() + self._credentials = credentials + + # Will automatically refresh if possible + frozen_credentials = self._credentials.get_frozen_credentials() + return dict( + accessKeyId=frozen_credentials.access_key, + secretAccessKey=frozen_credentials.secret_key, + token=frozen_credentials.token, + region=session.region_name, + ) + + return run_on_new_thread(func) + + +_global_aws_application_default_credentials_provider = None +_global_aws_application_default_credentials_provider_lock = threading.Lock() + + +def get_aws_application_default_credentials_provider(): + global _global_aws_application_default_credentials_provider + with _global_aws_application_default_credentials_provider_lock: + if _global_aws_application_default_credentials_provider is None: + _global_aws_application_default_credentials_provider = ( + AWSApplicationDefaultCredentialsProvider() + ) + return _global_aws_application_default_credentials_provider diff --git a/python/neuroglancer/default_credentials_manager.py b/python/neuroglancer/default_credentials_manager.py index 96139788d7..69f64acc2c 100644 --- a/python/neuroglancer/default_credentials_manager.py +++ b/python/neuroglancer/default_credentials_manager.py @@ -13,6 +13,7 @@ # limitations under the License. from . import ( + aws_credentials, boss_credentials, credentials_provider, dvid_credentials, @@ -46,6 +47,11 @@ ), ) +default_credentials_manager.register( + "s3", + lambda _parameters: aws_credentials.get_aws_application_default_credentials_provider(), +) + def set_boss_token(token): """Sets the authentication token for connecting to bossDB. diff --git a/python/neuroglancer/server.py b/python/neuroglancer/server.py index 11e7ea47d7..bb04781045 100644 --- a/python/neuroglancer/server.py +++ b/python/neuroglancer/server.py @@ -366,6 +366,33 @@ def post(self, viewer_token: str): class CredentialsHandler(BaseRequestHandler): + async def get(self, viewer_token: str): + viewer = self.server.viewers.get(viewer_token) + if viewer is None: + self.send_error(404) + return + if not viewer.allow_credentials: + self.send_error(403) + return + if self.server._credentials_manager is None: + from .default_credentials_manager import default_credentials_manager + + self.server._credentials_manager = default_credentials_manager + query = self.request.query + provider = self.server._credentials_manager.get(query, None) + if provider is None: + self.send_error(400) + return + try: + credentials = await asyncio.wrap_future(provider.get(False)) + self.set_header("Content-type", "application/json") + self.finish(json.dumps(credentials)) + except Exception: + import traceback + + traceback.print_exc() + self.send_error(401) + async def post(self, viewer_token: str): viewer = self.server.viewers.get(viewer_token) if viewer is None: diff --git a/src/credentials_provider/aws_sigv4.ts b/src/credentials_provider/aws_sigv4.ts new file mode 100644 index 0000000000..297bff0e62 --- /dev/null +++ b/src/credentials_provider/aws_sigv4.ts @@ -0,0 +1,126 @@ +/** + * @license + * Copyright 2020 Google Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Sha256 } from "@aws-crypto/sha256-js"; +import { SignatureV4 } from "@smithy/signature-v4"; +import type { HttpRequest } from "@smithy/types"; +import { + fetchOkWithCredentials, + fetchOkWithCredentialsAdapter, +} from "#src/credentials_provider/http_request.js"; +import type { CredentialsProvider } from "#src/credentials_provider/index.js"; +import type { FetchOk, HttpError } from "#src/util/http_request.js"; +import { fetchOk } from "#src/util/http_request.js"; + +//aws-sdk signature v4 signer + +/** + * AWS Access Tokens + */ +export interface AWSSignatureV4Credentials { + accessKeyId: string; + secretAccessKey: string; + region: string; + token?: string; // if provided, expiration should be provided as well + // AccountId?: string; + // Expiration?: string; // rfc3339 +} + +async function applyCredentials( + credentials: AWSSignatureV4Credentials, + init: RequestInit, + input: RequestInfo, +): Promise { + if (!credentials.accessKeyId) { + return init; + } + const signer = new SignatureV4({ + service: "s3", + region: credentials.region, + credentials: { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.token, + }, + sha256: Sha256, + }); + const apiUrl = new URL(input.toString()); + const request = { + hostname: apiUrl.hostname.toString(), + protocol: apiUrl.protocol, + path: apiUrl.pathname, + method: "GET", + headers: { ...init.headers, host: apiUrl.hostname.toString() }, + } as HttpRequest; + return signer + .sign(request) + .then((signedRequest) => { + const headers = signedRequest.headers; + const x = { ...init, headers }; + return x; + }) + .catch((error) => { + throw error; + }); +} + +function errorHandler( + error: HttpError, + credentials: AWSSignatureV4Credentials, +): "refresh" { + const { status } = error; + if (status === 401) { + // 401: Authorization needed. + return "refresh"; + } + if (status === 403 && !credentials.accessKeyId) { + // Anonymous access denied. Request credentials. + return "refresh"; + } + throw error; +} + +export function fetchOkWithAWSSignatureV4Credentials( + credentialsProvider: + | CredentialsProvider + | undefined, + input: RequestInfo, + init: RequestInit, +): Promise { + if (credentialsProvider === undefined) { + return fetchOk(input, init); + } + return fetchOkWithCredentials( + credentialsProvider, + input, + init, + applyCredentials, + errorHandler, + ); +} + +export function fetchOkWithAWSSignatureV4CredentialsAdapter( + credentialsProvider: + | CredentialsProvider + | undefined, +): FetchOk { + if (credentialsProvider === undefined) return fetchOk; + return fetchOkWithCredentialsAdapter( + credentialsProvider, + applyCredentials, + errorHandler, + ); +} diff --git a/src/credentials_provider/http_request.ts b/src/credentials_provider/http_request.ts index bdf8bec3f0..f94800dfb9 100644 --- a/src/credentials_provider/http_request.ts +++ b/src/credentials_provider/http_request.ts @@ -31,7 +31,8 @@ export async function fetchOkWithCredentials( applyCredentials: ( credentials: Credentials, requestInit: RequestInit & { progressListener?: ProgressListener }, - ) => RequestInit & { progressListener?: ProgressListener }, + requestInfo: RequestInfo, + ) => Promise, errorHandler: (httpError: HttpError, credentials: Credentials) => "refresh", ): Promise { let credentials: CredentialsWithGeneration | undefined; @@ -50,10 +51,14 @@ export async function fetchOkWithCredentials( progressListener: init.progressListener, }); try { - return await fetchOk( - typeof input === "function" ? input(credentials.credentials) : input, - applyCredentials(credentials.credentials, init), + const resolved_input = + typeof input === "function" ? input(credentials.credentials) : input; + const resolved_init = await applyCredentials( + credentials.credentials, + init, + resolved_input, ); + return await fetchOk(resolved_input, resolved_init); } catch (error) { if (error instanceof HttpError) { if (errorHandler(error, credentials.credentials) === "refresh") { @@ -71,7 +76,8 @@ export function fetchOkWithCredentialsAdapter( applyCredentials: ( credentials: Credentials, requestInit: RequestInit & { progressListener?: ProgressListener }, - ) => RequestInit & { progressListener?: ProgressListener }, + requestInfo: RequestInfo, + ) => Promise, errorHandler: (httpError: HttpError, credentials: Credentials) => "refresh", ): FetchOk { return (input, init = {}) => diff --git a/src/credentials_provider/oauth2.ts b/src/credentials_provider/oauth2.ts index 3fee4084fa..1776abcb52 100644 --- a/src/credentials_provider/oauth2.ts +++ b/src/credentials_provider/oauth2.ts @@ -31,10 +31,10 @@ export interface OAuth2Credentials { email?: string; } -function applyCredentials( +async function applyCredentials( credentials: OAuth2Credentials, init: RequestInit, -): RequestInit { +): Promise { if (!credentials.accessToken) return init; const headers = new Headers(init.headers); headers.set( diff --git a/src/datasource/boss/api.ts b/src/datasource/boss/api.ts index 8ca9ff6817..af89199d24 100644 --- a/src/datasource/boss/api.ts +++ b/src/datasource/boss/api.ts @@ -45,7 +45,7 @@ export async function fetchWithBossCredentials( credentialsProvider, input, init, - (credentials) => { + async (credentials) => { const headers = new Headers(init.headers); headers.set("Authorization", `Bearer ${credentials}`); return { ...init, headers }; diff --git a/src/datasource/dvid/api.ts b/src/datasource/dvid/api.ts index c9f0ed1ed2..5ba42f8103 100644 --- a/src/datasource/dvid/api.ts +++ b/src/datasource/dvid/api.ts @@ -80,7 +80,7 @@ export function fetchWithDVIDCredentials( credentialsProvider, input, init, - (credentials: DVIDToken, init: RequestInit) => { + async (credentials: DVIDToken, init: RequestInit) => { const newInit: RequestInit = { ...init }; if (credentials.token) { newInit.headers = { diff --git a/src/datasource/nggraph/frontend.ts b/src/datasource/nggraph/frontend.ts index 8578c9cf6d..8894c20b85 100644 --- a/src/datasource/nggraph/frontend.ts +++ b/src/datasource/nggraph/frontend.ts @@ -614,7 +614,7 @@ function fetchWithNggraphCredentials( credentialsProvider, `${serverUrl}${path}`, init, - (credentials, init) => { + async (credentials, init) => { const headers = new Headers(init.headers); headers.set("Authorization", credentials.token); return { ...init, headers }; diff --git a/src/kvstore/gcs/register.ts b/src/kvstore/gcs/register.ts index 4eb612bf77..6ab013628b 100644 --- a/src/kvstore/gcs/register.ts +++ b/src/kvstore/gcs/register.ts @@ -15,12 +15,15 @@ */ import pythonIntegration from "#python_integration_build"; +import type { CredentialsProvider } from "#src/credentials_provider/index.js"; +import type { OAuth2Credentials } from "#src/credentials_provider/oauth2.js"; +import { fetchOkWithOAuth2CredentialsAdapter } from "#src/credentials_provider/oauth2.js"; import type { BaseKvStoreProvider } from "#src/kvstore/context.js"; import { GcsKvStore } from "#src/kvstore/gcs/index.js"; import type { SharedKvStoreContextBase } from "#src/kvstore/register.js"; import { frontendBackendIsomorphicKvStoreProviderRegistry } from "#src/kvstore/register.js"; -function gcsProvider(_context: SharedKvStoreContextBase): BaseKvStoreProvider { +function gcsProvider(context: SharedKvStoreContextBase): BaseKvStoreProvider { return { scheme: "gs", description: pythonIntegration @@ -32,6 +35,18 @@ function gcsProvider(_context: SharedKvStoreContextBase): BaseKvStoreProvider { throw new Error("Invalid URL, expected `gs:///`"); } const [, bucket, path] = m; + if (pythonIntegration) { + const credentialsProvider: CredentialsProvider = + context.credentialsManager.getCredentialsProvider("gcs", { bucket }); + return { + store: new GcsKvStore( + bucket, + `gs://${bucket}/`, + fetchOkWithOAuth2CredentialsAdapter(credentialsProvider), + ), + path: decodeURIComponent((path ?? "").substring(1)), + }; + } return { store: new GcsKvStore(bucket), path: decodeURIComponent((path ?? "").substring(1)), diff --git a/src/kvstore/ngauth/credentials_provider.ts b/src/kvstore/ngauth/credentials_provider.ts index 7d2208f0ee..29c16a3a3e 100644 --- a/src/kvstore/ngauth/credentials_provider.ts +++ b/src/kvstore/ngauth/credentials_provider.ts @@ -160,7 +160,7 @@ export class NgauthGcsCredentialsProvider extends CredentialsProvider { + async (credentials, init) => { return { ...init, body: JSON.stringify({ diff --git a/src/kvstore/s3/common.ts b/src/kvstore/s3/common.ts index e1e19ca06f..b7b72b3e00 100644 --- a/src/kvstore/s3/common.ts +++ b/src/kvstore/s3/common.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import pythonIntegration from "#python_integration_build"; +import type { AWSSignatureV4Credentials } from "#src/credentials_provider/aws_sigv4.js"; +import { fetchOkWithAWSSignatureV4CredentialsAdapter } from "#src/credentials_provider/aws_sigv4.js"; +import type { CredentialsProvider } from "#src/credentials_provider/index.js"; import type { BaseKvStoreProvider } from "#src/kvstore/context.js"; import { read, stat } from "#src/kvstore/http/read.js"; import type { @@ -108,13 +112,29 @@ function amazonS3Provider< ): BaseKvStoreProvider { return { scheme: "s3", - description: "S3 (anonymous)", + description: pythonIntegration ? "AWS S3" : "AWS S3 (anonymous)", getKvStore(url) { const m = (url.suffix ?? "").match(/^\/\/([^/]+)(\/.*)?$/); if (m === null) { throw new Error("Invalid URL, expected `s3:///`"); } const [, bucket, path] = m; + if (pythonIntegration) { + const credentialsProvider: CredentialsProvider = + sharedKvStoreContext.credentialsManager.getCredentialsProvider("s3", { + bucket, + }); + return { + store: new s3KvStoreClass( + sharedKvStoreContext, + `https://${bucket}.s3.amazonaws.com/`, + `s3://${bucket}/`, + /*knownToBeVirtualHostedStyle=*/ true, + fetchOkWithAWSSignatureV4CredentialsAdapter(credentialsProvider), + ), + path: decodeURIComponent((path ?? "").substring(1)), + }; + } return { store: new s3KvStoreClass( sharedKvStoreContext, diff --git a/src/kvstore/s3/index.rst b/src/kvstore/s3/index.rst index 52bbbb165a..4e3029047f 100644 --- a/src/kvstore/s3/index.rst +++ b/src/kvstore/s3/index.rst @@ -43,7 +43,28 @@ Capabilities Authentication -------------- -Currently, only anonymous access is supported. +When not using the :ref:`Python API`: + +- The :file:`s3://{bucket}/{path}` syntax implies anonymous access, meaning the + :file:`{bucket}` must allow public read access without `requester pays + `__. Refer to the AWS + documentation for `details on making buckets publicly accessible + `__. + +When using the :ref:`Python API` with credentials enabled: + +- The :file:`s3://{bucket}/{path}` syntax uses the `AWS Default + Credentials + `__, + if available. + + +Required permissions +-------------------- + +- The ``s3:GetObject`` permission is required for reading. +- Additionally, the ``s3:ListBucket`` permission is required for listing + directories. CORS ---- diff --git a/src/python_integration/credentials_provider.ts b/src/python_integration/credentials_provider.ts index 478bf12739..6784167859 100644 --- a/src/python_integration/credentials_provider.ts +++ b/src/python_integration/credentials_provider.ts @@ -75,6 +75,17 @@ class GcsCredentialsProvider extends AnonymousFirstCredentialsProvider { } } +class AwsCredentialsProvider extends AnonymousFirstCredentialsProvider { + constructor(baseProvider: CredentialsProvider) { + super(baseProvider, { + accessKey: "", + secretKey: "", + token: "", + tokenType: "", + }); + } +} + export class PythonCredentialsManager implements CredentialsManager { constructor(private client: Client) {} private memoize = new Memoize>(); @@ -93,6 +104,9 @@ export class PythonCredentialsManager implements CredentialsManager { if (key === "gcs") { return new GcsCredentialsProvider(provider); } + if (key === "s3") { + return new AwsCredentialsProvider(provider); + } return provider; }); } diff --git a/uv.lock b/uv.lock index da5fae227a..79202d024c 100644 --- a/uv.lock +++ b/uv.lock @@ -102,6 +102,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, ] +[[package]] +name = "boto3" +version = "1.37.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/36/03f64be665ae27d149ade9fe1ec5d4c83101fb26865d1246c81b6a399882/boto3-1.37.12.tar.gz", hash = "sha256:9412d404f103ad6d14f033eb29cd5e0cdca2b9b08cbfa9d4dabd1d7be2de2625", size = 111402 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/1c/edfd0b54395849c57fdb21f1db697d05c04d0c4b957111130e03e74d2807/boto3-1.37.12-py3-none-any.whl", hash = "sha256:516feaa0d2afaeda1515216fd09291368a1215754bbccb0f28414c0a91a830a2", size = 139551 }, +] + +[[package]] +name = "botocore" +version = "1.37.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/12/7e685d2f32f646ddcde297c18c52122a5c8676105cb876b6702e515857ad/botocore-1.37.12.tar.gz", hash = "sha256:ae2d5328ce6ad02eb615270507235a6e90fd3eeed615a6c0732b5a68b12f2017", size = 13648734 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/45/7db93d9b0b2d9060684af08d8fba1245b47fed41d7ed8c3d9f5c7efbe261/botocore-1.37.12-py3-none-any.whl", hash = "sha256:ba1948c883bbabe20d95ff62c3e36954c9269686f7db9361857835677ca3e676", size = 13410913 }, +] + +[[package]] +name = "botocore-stubs" +version = "1.37.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-awscrt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/29/756631d4f16b80c0b14fc862dd4ac6294779ca0dff5b3c65c0ebf752c282/botocore_stubs-1.37.13.tar.gz", hash = "sha256:ea1e9a380e4119c570cc077a94c737727d8c81e0b867a3d75dd94beb17bb17b7", size = 42115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/5b/f0ae52bcfba7af5ca6ba9052e495072bb9cfad4119f3b0751fe59ad4e1c8/botocore_stubs-1.37.13-py3-none-any.whl", hash = "sha256:cf0cd46a722b831397758d5b34a2d68543e76ac881213784e474a3d9340a2d72", size = 65386 }, +] + [[package]] name = "bracex" version = "2.5.post1" @@ -388,6 +428,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, ] +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + [[package]] name = "jsonschema" version = "4.23.0" @@ -555,6 +604,7 @@ name = "neuroglancer" source = { editable = "." } dependencies = [ { name = "atomicwrites" }, + { name = "boto3" }, { name = "google-apitools" }, { name = "google-auth" }, { name = "numpy" }, @@ -589,6 +639,7 @@ mypy = [ { name = "mypy" }, { name = "pandas-stubs" }, { name = "types-atomicwrites" }, + { name = "types-boto3" }, { name = "types-pillow" }, { name = "types-requests" }, { name = "types-setuptools" }, @@ -606,6 +657,7 @@ test = [ [package.metadata] requires-dist = [ { name = "atomicwrites", specifier = ">=1.4.1" }, + { name = "boto3", specifier = ">=1.31.16" }, { name = "google-apitools", specifier = ">=0.5.32" }, { name = "google-auth", specifier = ">=2.38.0" }, { name = "numpy", specifier = ">=1.11.0" }, @@ -635,6 +687,7 @@ mypy = [ { name = "mypy", specifier = ">=1.14.1" }, { name = "pandas-stubs", specifier = ">=2.2.3.241126" }, { name = "types-atomicwrites", specifier = ">=1.4.5.1" }, + { name = "types-boto3", specifier = ">=1.31.16" }, { name = "types-pillow", specifier = ">=10.2.0.20240822" }, { name = "types-requests", specifier = ">=2.32.0.20241016" }, { name = "types-setuptools", specifier = ">=75.8.0.20250110" }, @@ -1054,6 +1107,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1248,6 +1313,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/4b/cb63278d66afe2137d042a03a92640a7190d51cf4ea19bdf0830ecf3625c/ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b", size = 7230080 }, ] +[[package]] +name = "s3transfer" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/ec/aa1a215e5c126fe5decbee2e107468f51d9ce190b9763cb649f76bb45938/s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679", size = 148419 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/62/8d3fc3ec6640161a5649b2cddbbf2b9fa39c92541225b33f117c37c5a2eb/s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d", size = 84412 }, +] + [[package]] name = "selenium" version = "4.29.0" @@ -1539,6 +1616,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/18/3e6d9e83ef176dfbf59152086f7629c344b6384ece6044bfb411f4ffd57a/types_atomicwrites-1.4.5.1-py3-none-any.whl", hash = "sha256:2f1febbdc78b55453b189fa5b136dce34bab7d1d82319163d470e404aab55c83", size = 2528 }, ] +[[package]] +name = "types-awscrt" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/2d/393ac8f215417bc532ef9451ba42f148ecedcd5ff91095d8640042ecae2c/types_awscrt-0.24.2.tar.gz", hash = "sha256:5826baf69ad5d29c76be49fc7df00222281fa31b14f99e9fb4492d71ec98fea5", size = 15441 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/33/e2b7dcb6acc3ba8e8191571c38d2d64fc0822e8fd53fff0f2736859abef6/types_awscrt-0.24.2-py3-none-any.whl", hash = "sha256:345ab84a4f75b26bfb816b249657855824a4f2d1ce5b58268c549f81fce6eccc", size = 19414 }, +] + +[[package]] +name = "types-boto3" +version = "1.37.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "types-s3transfer" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/3d/bfb8773e191f2002814771c57324a5bfa1df48b7850a7f479ca1f6bea2e4/types_boto3-1.37.13.tar.gz", hash = "sha256:003304034d45b03c16816e9549c7cff751ec81948f14858c4e4621656d7a98ad", size = 99316 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/42/7bafb6dfa1c5069aefcad09f8d3280b2abada16b1818d86f64d4549171d8/types_boto3-1.37.13-py3-none-any.whl", hash = "sha256:29551d6a8861806970a7cd49c142cd46fffa99ff2c64104acc3d7d8974369994", size = 68320 }, +] + [[package]] name = "types-pillow" version = "10.2.0.20240822" @@ -1569,6 +1669,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/01/485b3026ff90e5190b5e24f1711522e06c79f4a56c8f4b95848ac072e20f/types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747", size = 15836 }, ] +[[package]] +name = "types-s3transfer" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/a9/440d8ba72a81bcf2cc5a56ef63f23b58ce93e7b9b62409697553bdcdd181/types_s3transfer-0.11.4.tar.gz", hash = "sha256:05fde593c84270f19fd053f0b1e08f5a057d7c5f036b9884e68fb8cd3041ac30", size = 14074 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/69/0b5ae42c3c33d31a32f7dcb9f35a3e327365360a6e4a2a7b491904bd38aa/types_s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:2a76d92c07d4a3cb469e5343b2e7560e0b8078b2e03696a65407b8c44c861b61", size = 19516 }, +] + [[package]] name = "types-setuptools" version = "75.8.0.20250110" From 0d9370a863922f4f6d11a41102598939cda202b6 Mon Sep 17 00:00:00 2001 From: Matthew Aitken Date: Mon, 17 Mar 2025 11:36:18 -0700 Subject: [PATCH 2/3] remove credential get api added for debugggin --- python/neuroglancer/server.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/python/neuroglancer/server.py b/python/neuroglancer/server.py index bb04781045..11e7ea47d7 100644 --- a/python/neuroglancer/server.py +++ b/python/neuroglancer/server.py @@ -366,33 +366,6 @@ def post(self, viewer_token: str): class CredentialsHandler(BaseRequestHandler): - async def get(self, viewer_token: str): - viewer = self.server.viewers.get(viewer_token) - if viewer is None: - self.send_error(404) - return - if not viewer.allow_credentials: - self.send_error(403) - return - if self.server._credentials_manager is None: - from .default_credentials_manager import default_credentials_manager - - self.server._credentials_manager = default_credentials_manager - query = self.request.query - provider = self.server._credentials_manager.get(query, None) - if provider is None: - self.send_error(400) - return - try: - credentials = await asyncio.wrap_future(provider.get(False)) - self.set_header("Content-type", "application/json") - self.finish(json.dumps(credentials)) - except Exception: - import traceback - - traceback.print_exc() - self.send_error(401) - async def post(self, viewer_token: str): viewer = self.server.viewers.get(viewer_token) if viewer is None: From 1260a99c648446f3850468a655986e8931a8928b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 4 Jun 2025 20:23:39 -0700 Subject: [PATCH 3/3] Update src/credentials_provider/aws_sigv4.ts Co-authored-by: Emanuele Bezzi --- src/credentials_provider/aws_sigv4.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/credentials_provider/aws_sigv4.ts b/src/credentials_provider/aws_sigv4.ts index 297bff0e62..243f803f0a 100644 --- a/src/credentials_provider/aws_sigv4.ts +++ b/src/credentials_provider/aws_sigv4.ts @@ -62,7 +62,7 @@ async function applyCredentials( hostname: apiUrl.hostname.toString(), protocol: apiUrl.protocol, path: apiUrl.pathname, - method: "GET", + method: init.method || "GET", headers: { ...init.headers, host: apiUrl.hostname.toString() }, } as HttpRequest; return signer