Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
32ea660
add api route for retrieving a bucket policy
AndrewPlayer3 Mar 13, 2026
3eba802
formatting
AndrewPlayer3 Mar 13, 2026
c9656f9
add boto3 req
AndrewPlayer3 Mar 13, 2026
9795e68
add bucket and bucket prefix from api ref
AndrewPlayer3 Mar 26, 2026
a7bf25d
pass content bucket to api
AndrewPlayer3 Mar 26, 2026
2d396a2
add user provided publish bucket option for all jobs
AndrewPlayer3 Mar 26, 2026
e223823
add bucket and bucket_prefix to batch params
AndrewPlayer3 Mar 26, 2026
b49e150
bucket read permissions
AndrewPlayer3 Mar 26, 2026
53f77c3
cleaner get_files handler
AndrewPlayer3 Mar 26, 2026
c8c14ab
cleaner handler for content bucket
AndrewPlayer3 Apr 1, 2026
f8ae4cf
changed defaults for bucket and prefix
AndrewPlayer3 Apr 1, 2026
5175a22
add return type for get_current_account_arn
AndrewPlayer3 Apr 1, 2026
86dfb41
add return type for get_bucket_policy
AndrewPlayer3 Apr 1, 2026
c0d180d
ruff
AndrewPlayer3 Apr 1, 2026
a534722
add return type for _handle_content_bucket
AndrewPlayer3 Apr 1, 2026
de28431
mypy
AndrewPlayer3 Apr 1, 2026
34be2fd
better regex for bucket and bucket prefix
AndrewPlayer3 Apr 2, 2026
4c39afb
fixed regex escape characters
AndrewPlayer3 Apr 2, 2026
1268937
use enumerate rather than range
AndrewPlayer3 Apr 2, 2026
18dd47d
move bucket handling to dynamo, handle nulls, and add env var for tests
AndrewPlayer3 Apr 2, 2026
800c72c
update tests for bucket and bucket_prefix handling
AndrewPlayer3 Apr 2, 2026
2f6e8d9
update test_put_jobs
AndrewPlayer3 Apr 2, 2026
db70009
cleaner _handle_content_bucket and fixed test
AndrewPlayer3 Apr 2, 2026
fa0b2ae
add error for attempting to use custom prefix with default bucket
AndrewPlayer3 Apr 2, 2026
991eea4
add ref to content bucket for api
AndrewPlayer3 Apr 2, 2026
7e03b40
fixed test_put_jobs credit count
AndrewPlayer3 Apr 2, 2026
90d2e9c
updated changelog
AndrewPlayer3 Apr 2, 2026
639e99b
add patch for dynamo.jobs.get_jobs
AndrewPlayer3 Apr 3, 2026
425a610
revert --bucket-prefix to --bucket_prefix
AndrewPlayer3 Apr 3, 2026
e0a6f4f
remove todo
AndrewPlayer3 Apr 3, 2026
69ad74c
removed comment
AndrewPlayer3 Apr 3, 2026
f669e7f
fix boto3 conflicts after rebase
jhkennedy May 13, 2026
39d6373
Update permissions for HyP3 + S3 interactions outside the hyp3 conten…
jhkennedy May 13, 2026
2b62ad2
add pydantic to fix pip errors
jhkennedy May 14, 2026
9fc2fa5
try removing openapi decorator
jhkennedy May 14, 2026
2eafd48
fix get_caller_identity
jhkennedy May 14, 2026
bfd2d93
Return dictionaries from bucket-policy handler instead of a big string
jhkennedy May 14, 2026
c93f210
Simplify bucket-policy response
jhkennedy May 14, 2026
0042abd
Add dynamodb:GetItem permissions to get-files
jhkennedy May 14, 2026
485d413
put the logs in the user bucket as well
jhkennedy May 14, 2026
0e7f7b4
actually, use event context instead of dynamo.jobs.get_job for get-files
jhkennedy May 14, 2026
f0aa3ef
add bucket and bucket_prefix as parameters to get-files and upload-lo…
jhkennedy May 14, 2026
d6d412b
tweak bucket-policy
jhkennedy May 14, 2026
84ad852
Fix get-files: exiration time optional and fix distribution url
jhkennedy May 15, 2026
d931c74
ruff ruff
jhkennedy May 15, 2026
ac1756d
update get-files and upload-logs tests
jhkennedy May 15, 2026
4648b8b
remove redundant parameter from upload-logs step in step function
jhkennedy May 15, 2026
084e04b
remove prefix == job_id assumption in upload logs
jhkennedy May 15, 2026
73624e8
Add OpenAPI spec for bucket-policy
jhkennedy May 18, 2026
521887a
Add OpenAPI spec for bucket-policy GET endpoint
jhkennedy May 18, 2026
f1bf40b
Add note to handlers to also update openapi spec
jhkennedy May 18, 2026
6b015b5
Fix bucket + bucket_prefix in some job specs
jhkennedy May 19, 2026
1142386
Merge pull request #3102 from ASFHyP3/user-bucket-permissions
AndrewPlayer3 May 20, 2026
4919ea6
Merge branch 'develop' into user-defined-buckets
jhkennedy May 20, 2026
9201162
Drop publish_bucket FIXME reminder comments
jhkennedy May 20, 2026
6d98205
Merge pull request #3061 from ASFHyP3/user-defined-buckets
jhkennedy May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [10.17.0]

### Added
- `bucket` and `bucket_prefix` top level parameters for all job types.
- The `bucket` parameter allows for overwriting the content bucket that a job's products will be placed into.
- The `bucket_prefix` parameter:
- allows for overwriting the default prefix (the job ID) for the S3 bucket.
- can only be used if also using a custom bucket.
- allows for inserting the job's ID and name by including `{job_id}` or `{name}` in the prefix string.
- `/bucket-policy/<bucket_name>` route for retrieving an AWS policy that will allow HyP3 to write to a the `<bucket_name>` S3 Bucket.

## [10.16.5]

### Added
Expand Down
4 changes: 4 additions & 0 deletions apps/api/api-cf.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Parameters:
SystemAvailable:
Type: String

ContentBucket:
Type: String

{% if security_environment == 'EDC' %}
VpcId:
Type: String
Expand Down Expand Up @@ -193,6 +196,7 @@ Resources:
DEFAULT_CREDITS_PER_USER: !Ref DefaultCreditsPerUser
DEFAULT_APPLICATION_STATUS: !Ref DefaultApplicationStatus
SYSTEM_AVAILABLE: !Ref SystemAvailable
CONTENT_BUCKET: !Ref ContentBucket
Code: src/
Handler: hyp3_api.lambda_handler.handler
MemorySize: 3008
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/hyp3_api/api-spec/job_parameters.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ components:
- {{ job_type }}
name:
$ref: "./openapi-spec.yml#/components/schemas/name"
bucket:
$ref: "./openapi-spec.yml#/components/schemas/bucket"
bucket_prefix:
$ref: "./openapi-spec.yml#/components/schemas/bucket_prefix"
job_parameters:
$ref: "#/components/schemas/{{ job_type }}Parameters"

Expand Down
88 changes: 86 additions & 2 deletions apps/api/src/hyp3_api/api-spec/openapi-spec.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ paths:
in: query
schema:
$ref: "#/components/schemas/start_token"

responses:
"200":
description: 200 response
Expand Down Expand Up @@ -136,7 +135,6 @@ paths:
schema:
$ref: "#/components/schemas/job_id"
required: true

responses:
"200":
description: 200 response
Expand Down Expand Up @@ -172,6 +170,23 @@ paths:
schema:
$ref: "#/components/schemas/user"

/bucket-policy/{bucket_name}:
get:
description: Get a bucket policy for `bucket_name` which allows HyP3 to write to that bucket.
parameters:
- name: bucket_name
in: path
schema:
$ref: "#/components/schemas/bucket"
required: true
responses:
"200":
description: 200 response
content:
application/json:
schema:
$ref: "#/components/schemas/bucket_policy"

components:
schemas:

Expand Down Expand Up @@ -354,6 +369,10 @@ components:
$ref: "#/components/schemas/execution_started"
name:
$ref: "#/components/schemas/name"
bucket:
$ref: "#/components/schemas/bucket"
bucket_prefix:
$ref: "#/components/schemas/bucket_prefix"
files:
$ref: "#/components/schemas/list_of_files"
browse_images:
Expand Down Expand Up @@ -438,6 +457,71 @@ components:
maxLength: 100
example: Job Name

bucket:
description: User provided text to define the product bucket, null for default
type: string
nullable: true
minLength: 3
maxLength: 63
example: "my-example-bucket"
pattern: "(?!(^((2(5[0-5]|[0-4][0-9])|[01]?[0-9]{1,2}).){3}(2(5[0-5]|[0-4][0-9])|[01]?[0-9]{1,2})$|^xn--|.+-s3alias$))^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$"

bucket_prefix:
description: User provided text to define the bucket prefix to place the product in
type: string
nullable: true
minLength: 1
maxLength: 100
example: "{job_id}"
pattern: "^(?!\/)(?!.*\/\/)(?:[A-Za-z0-9._\/-]|{job_id}|{name}){1,1024}$"

bucket_policy:
description: An AWS S3 Bucket Policy that, when applied, will allow HyP3 to write to the bucket.
type: object
required:
- Version
- Statement
additionalProperties: false
properties:
Version:
type: string
format: date
example: "2012-10-17"
Statement:
type: array
items:
type: object
example: >-
[
{
"Sid": "HyP3 bucket-level publish permissions",
"Effect": "Allow",
"Principal": {
"AWS": "123456789012"
},
"Action": [
"s3:ListBucket",
"s3:getBucketLocation"
],
"Resource": "arn:aws:s3:::my-example-bucket"
},
{
"Sid": "HyP3 object-level publish permissions",
"Effect": "Allow",
"Principal": {
"AWS": "123456789012"
},
"Action": [
"s3:GetObject",
"s3:GetObjectTagging",
"s3:PutObject",
"s3:PutObjectTagging"
],
"Resource": "arn:aws:s3:::my-example-bucket/*"
}
]


start_token:
description: Token used for fetching subsequent results for large queries
type: string
Expand Down
42 changes: 40 additions & 2 deletions apps/api/src/hyp3_api/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dynamo
from dynamo.exceptions import (
AccessCodeError,
CustomPrefixForDefaultBucketError,
InsufficientCreditsError,
UnexpectedApplicationStatusError,
UpdateJobForDifferentUserError,
Expand All @@ -24,20 +25,20 @@ def problem_format(status: int, message: str) -> Response:

def post_jobs(body: dict, user: str) -> dict:
print(body)

try:
validate_jobs(body['jobs'])
except CmrError as e:
abort(problem_format(503, str(e)))
except (ValidationError, MultiBurstValidationError) as e:
abort(problem_format(400, str(e)))

try:
body['jobs'] = dynamo.jobs.put_jobs(user, body['jobs'], dry_run=bool(body.get('validate_only')))
except UnexpectedApplicationStatusError as e:
abort(problem_format(403, str(e)))
except InsufficientCreditsError as e:
abort(problem_format(400, str(e)))
except CustomPrefixForDefaultBucketError as e:
abort(problem_format(400, str(e)))
return body


Expand Down Expand Up @@ -132,3 +133,40 @@ def _get_names_for_user(user: str) -> list[str]:
jobs.extend(new_jobs)
names = {job['name'] for job in jobs if 'name' in job}
return sorted(list(names))


def get_bucket_policy(bucket_name: str) -> dict:
account_arn = util.get_current_account_arn()
# NOTE: Reflect any edits here in api-spec/openapi-spec.yml.j2 as well
policy = {
'Version': '2012-10-17',
'Statement': [
{
'Sid': 'HyP3 bucket-level publish permissions',
'Effect': 'Allow',
'Principal': {
'AWS': f'{account_arn}',
},
'Action': [
's3:ListBucket',
's3:getBucketLocation',
],
'Resource': f'arn:aws:s3:::{bucket_name}',
},
{
'Sid': 'HyP3 object-level publish permissions',
'Effect': 'Allow',
'Principal': {
'AWS': f'{account_arn}',
},
'Action': [
's3:GetObject',
's3:GetObjectTagging',
's3:PutObject',
's3:PutObjectTagging',
],
'Resource': f'arn:aws:s3:::{bucket_name}/*',
},
],
}
return policy
6 changes: 6 additions & 0 deletions apps/api/src/hyp3_api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,9 @@ def user_patch() -> Response:
@openapi
def user_get() -> Response:
return jsonify(handlers.get_user(g.user))


@app.route('/bucket-policy/<bucket_name>', methods=['GET'])
@openapi
def bucket_policy_get(bucket_name: str) -> Response:
return jsonify(handlers.get_bucket_policy(bucket_name))
7 changes: 7 additions & 0 deletions apps/api/src/hyp3_api/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import Any
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse

import boto3


class TokenDeserializeError(Exception):
"""Raised when paging results and `start_token` fails to deserialize."""
Expand Down Expand Up @@ -46,3 +48,8 @@ def build_next_url(url: str, start_token: str, x_forwarded_host: str | None = No
url_parts[4] = urlencode(query)

return urlunparse(url_parts)


def get_current_account_arn() -> str:
sts = boto3.client('sts')
return sts.get_caller_identity()['Account']
16 changes: 13 additions & 3 deletions apps/compute-cf.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,20 @@ Resources:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: s3:PutObject
Resource: !Sub "arn:aws:s3:::${ContentBucket}/*"
Action:
- s3:ListBucket
- s3:GetObject
- s3:GetObjectTagging
- s3:PutObject
- s3:PutObjectTagging
Resource: "*"
Condition:
StringNotEquals:
s3:ResourceAccount: !Ref "AWS::AccountId"
- Effect: Allow
Action: s3:PutObjectTagging
Action:
- s3:PutObject
- s3:PutObjectTagging
Resource: !Sub "arn:aws:s3:::${ContentBucket}/*"
- Effect: Allow
Action: sns:Publish
Expand Down
14 changes: 13 additions & 1 deletion apps/get-files/get-files-cf.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,25 @@ Resources:
- logs:PutLogEvents
Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"
- Effect: Allow
Action: s3:ListBucket
Action:
- s3:ListBucket
- s3:getBucketLocation
Resource: !Sub "arn:aws:s3:::${Bucket}"
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectTagging
Resource: !Sub "arn:aws:s3:::${Bucket}/*"
- Effect: Allow
Action:
- s3:ListBucket
- s3:getBucketLocation
- s3:GetObject
- s3:GetObjectTagging
Resource: "*"
Condition:
StringNotEquals:
s3:ResourceAccount: !Ref "AWS::AccountId"
- Effect: Allow
Action: dynamodb:UpdateItem
Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${JobsTable}*"
Expand Down
23 changes: 13 additions & 10 deletions apps/get-files/src/get_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@


def get_download_url(bucket: str, key: str) -> str:
if distribution_url := os.getenv('DISTRIBUTION_URL'):
download_url = urllib.parse.urljoin(distribution_url, key)
else:
region = environ['AWS_REGION']
download_url = f'https://{bucket}.s3.{region}.amazonaws.com/{key}'
return download_url
if (bucket == environ['BUCKET']) and (distribution_url := os.getenv('DISTRIBUTION_URL')):
return urllib.parse.urljoin(distribution_url, key)

region = S3_CLIENT.head_bucket(Bucket=bucket)['BucketRegion']
return f'https://{bucket}.s3.{region}.amazonaws.com/{key}'


def get_expiration_time(bucket: str, key: str) -> str:
s3_object = S3_CLIENT.get_object(Bucket=bucket, Key=key)
expiration = s3_object.get('Expiration')
if expiration is None:
# HyP3's 100th birthday; 100 years since first non-ASF job
# https://hyp3-api.asf.alaska.edu/jobs/969ab836-aa95-4613-8673-2a0415949afa
return '2120-10-21T00:00:00+00:00'

expiration_string = s3_object['Expiration'].split('"')[1]
expiration_datetime = datetime.strptime(expiration_string, '%a, %d %b %Y %H:%M:%S %Z')
return expiration_datetime.isoformat(timespec='seconds') + '+00:00'
Expand Down Expand Up @@ -91,8 +96,6 @@ def organize_files(s3_objects: list[dict], bucket: str) -> dict:


def lambda_handler(event: dict, context: object) -> None:
bucket = environ['BUCKET']

response = S3_CLIENT.list_objects_v2(Bucket=bucket, Prefix=event['job_id'])
files = organize_files(response['Contents'], bucket)
response = S3_CLIENT.list_objects_v2(Bucket=event['bucket'], Prefix=event['bucket_prefix'])
files = organize_files(response['Contents'], event['bucket'])
dynamo.jobs.update_job({'job_id': event['job_id'], **files})
1 change: 1 addition & 0 deletions apps/main-cf.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ Resources:
DefaultCreditsPerUser: !Ref DefaultCreditsPerUser
DefaultApplicationStatus: !Ref DefaultApplicationStatus
SystemAvailable: !Ref SystemAvailable
ContentBucket: !Ref ContentBucket
{% if security_environment == 'EDC' %}
VpcId: !Ref VpcId
SecurityGroupId: !GetAtt Cluster.Outputs.SecurityGroupId
Expand Down
4 changes: 4 additions & 0 deletions apps/render_cf.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ def get_batch_job_parameters(job_spec: dict, step: dict, map_item: str | None =
for param in step_params:
if param == 'job_id':
batch_params['job_id.$'] = '$.job_id'
elif param == 'bucket':
batch_params['bucket.$'] = '$.bucket'
elif param == 'bucket_prefix':
batch_params['bucket_prefix.$'] = '$.bucket_prefix'
elif param == map_item:
batch_params[f'{map_item}.$'] = "States.Format('{}', $$.Map.Item.Value)"
else:
Expand Down
Loading
Loading