diff --git a/skeletonservice/config.py b/skeletonservice/config.py index 3ec61b5..f450eb2 100644 --- a/skeletonservice/config.py +++ b/skeletonservice/config.py @@ -43,6 +43,10 @@ class BaseConfig(object): LIMITER_CATEGORIES = "CONFIGURE_ME" LIMITER_URI = "CONFIGURE_ME" + # Lifetime of downscoped GCS tokens issued by the skeleton token endpoint (in minutes). + # Clients should treat tokens as expired after this many minutes. Defaults to 1 hour. + SKELETON_TOKEN_EXPIRATION_MINUTES = 60 + NEUROGLANCER_URL = "https://neuroglancer-demo.appspot.com" if os.environ.get("DAF_CREDENTIALS", None) is not None: with open(os.environ.get("DAF_CREDENTIALS"), "r") as f: diff --git a/skeletonservice/datasets/api.py b/skeletonservice/datasets/api.py index 640d330..f7ed39f 100644 --- a/skeletonservice/datasets/api.py +++ b/skeletonservice/datasets/api.py @@ -966,7 +966,7 @@ class SkeletonResource__get_skeleton_token(Resource): ] @staticmethod - def process(datastack_name: str, skvn: int, rids: list, verbose_level: int = 0): + def process(datastack_name: str, skvn: int, rids: list, expiration_minutes: int = 60, verbose_level: int = 0): SkelClassVsn = SkeletonService.get_version_specific_handler(skvn) return SkelClassVsn.get_skeleton_token_by_datastack_and_rids( @@ -977,6 +977,7 @@ def process(datastack_name: str, skvn: int, rids: list, verbose_level: int = 0): collapse_soma=True, collapse_radius=7500, skeleton_version=skvn, + expiration_minutes=expiration_minutes, session_timestamp_=SkeletonService.get_session_timestamp(), verbose_level_=verbose_level, ) @@ -991,4 +992,5 @@ def post(self, datastack_name: str, skvn: int): rids = rids[:MAX_BULK_CACHED_SKELETONS] verbose_level = int(data.get("verbose_level", 0)) verbose_level = max(int(request.args.get("verbose_level", 0)), verbose_level) - return self.process(datastack_name, skvn, rids, verbose_level) + expiration_minutes = int(current_app.config.get("SKELETON_TOKEN_EXPIRATION_MINUTES", 60)) + return self.process(datastack_name, skvn, rids, expiration_minutes, verbose_level) diff --git a/skeletonservice/datasets/service.py b/skeletonservice/datasets/service.py index 817a528..9dad043 100644 --- a/skeletonservice/datasets/service.py +++ b/skeletonservice/datasets/service.py @@ -2479,6 +2479,7 @@ def get_skeleton_token_by_datastack_and_rids( collapse_soma: bool, collapse_radius: int, skeleton_version: int = 0, + expiration_minutes: int = 60, session_timestamp_: str = "not_provided", verbose_level_: int = 0, ): @@ -2495,11 +2496,14 @@ def get_skeleton_token_by_datastack_and_rids( (V{HIGHEST_SKELETON_VERSION}). If a different skeleton_version is requested, it will be overridden to HIGHEST_SKELETON_VERSION. + The token lifetime is capped at expiration_minutes (server-side config, default 60). + Returns a dict: "token": short-lived Bearer token "token_type": "Bearer" "expiry": ISO-8601 expiry datetime string "bucket": GCS bucket name (without gs:// scheme) + "skeleton_version": skeleton version used (always HIGHEST_SKELETON_VERSION) "object_paths": {rid: GCS object path within the bucket} "missing": [rids not found in cache] """ @@ -2591,7 +2595,18 @@ def get_skeleton_token_by_datastack_and_rids( ) downscoped_creds.refresh(google.auth.transport.requests.Request()) - expiry_str = downscoped_creds.expiry.isoformat() if downscoped_creds.expiry else None + # Cap the reported expiry to the server-configured expiration_minutes. + # Clients should treat the token as invalid after this time. + max_expiry = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=expiration_minutes) + if downscoped_creds.expiry is not None: + cred_expiry = downscoped_creds.expiry + # google-auth returns naive UTC datetimes; make timezone-aware for comparison + if cred_expiry.tzinfo is None: + cred_expiry = cred_expiry.replace(tzinfo=datetime.timezone.utc) + effective_expiry = min(cred_expiry, max_expiry) + else: + effective_expiry = max_expiry + expiry_str = effective_expiry.isoformat() return { "token": downscoped_creds.token,