Skip to content

Commit 882dd24

Browse files
committed
More progress
1 parent 7d4177b commit 882dd24

File tree

2 files changed

+131
-47
lines changed

2 files changed

+131
-47
lines changed

src/stac_auth_proxy/app.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
6262
S3AssetSigner(bucket_pattern=settings.signer_asset_expression).endpoint,
6363
methods=["POST"],
6464
)
65-
app.add_middleware(
66-
AuthenticationExtensionMiddleware,
67-
endpoint=settings.signer_endpoint,
68-
asset_expression=settings.signer_asset_expression,
69-
)
7065

7166
app.add_api_route(
7267
"/{path:path}",
@@ -77,6 +72,15 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
7772
#
7873
# Middleware (order is important, last added = first to run)
7974
#
75+
app.add_middleware(
76+
AuthenticationExtensionMiddleware,
77+
signing_endpoint=settings.signer_endpoint,
78+
signed_asset_expression=settings.signer_asset_expression,
79+
default_public=settings.default_public,
80+
public_endpoints=settings.public_endpoints,
81+
private_endpoints=settings.private_endpoints,
82+
)
83+
8084
if settings.openapi_spec_endpoint:
8185
app.add_middleware(
8286
OpenApiMiddleware,
Lines changed: 122 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,153 @@
11
"""Middleware to add auth information to item response served by upstream API."""
22

3+
import logging
34
import re
45
from dataclasses import dataclass
5-
from typing import Any
6+
from itertools import chain
7+
from typing import Any, Optional
8+
from urllib.parse import urlparse
69

710
from starlette.requests import Request
811
from starlette.types import ASGIApp
912

10-
from ..utils.filters import is_item_endpoint
13+
from ..config import EndpointMethods
1114
from ..utils.middleware import JsonResponseMiddleware
15+
from ..utils.requests import find_match
16+
17+
logger = logging.getLogger(__name__)
1218

1319

1420
@dataclass(frozen=True)
1521
class AuthenticationExtensionMiddleware(JsonResponseMiddleware):
1622
"""Middleware to add the authentication extension to the response."""
1723

1824
app: ASGIApp
19-
endpoint: str
20-
asset_expression: str
25+
26+
signing_endpoint: Optional[str]
27+
signed_asset_expression: str
28+
29+
default_public: bool
30+
private_endpoints: EndpointMethods
31+
public_endpoints: EndpointMethods
32+
33+
signing_scheme: str = "signed_url_auth"
34+
auth_scheme: str = "oauth"
2135

2236
def should_transform_response(self, request: Request) -> bool:
23-
"""Only transform responses for STAC Items."""
24-
return is_item_endpoint(request.url.path)
37+
"""Determine if the response should be transformed."""
38+
print(f"{request.url=!s}")
39+
return True
2540

26-
def transform_json(self, item: dict[str, Any]) -> dict[str, Any]:
41+
def transform_json(self, doc: dict[str, Any]) -> dict[str, Any]:
2742
"""Augment the STAC Item with auth information."""
2843
extension = (
2944
"https://stac-extensions.github.io/authentication/v1.1.0/schema.json"
3045
)
31-
extensions = item.setdefault("stac_extensions", [])
46+
extensions = doc.setdefault("stac_extensions", [])
3247
if extension not in extensions:
3348
extensions.append(extension)
3449

3550
# TODO: Should we add this to items even if the assets don't match the asset expression?
36-
schemes = item["properties"].setdefault("auth:schemes", {})
37-
scheme = "signed_url_auth"
38-
schemes[scheme] = {
39-
"type": "signedUrl",
40-
"description": "Requires an authentication API",
41-
"flows": {
42-
"authorizationCode": {
43-
"authorizationApi": self.endpoint,
44-
"method": "POST",
45-
"parameters": {
46-
"bucket": {
47-
"in": "body",
48-
"required": True,
49-
"description": "asset bucket",
50-
"schema": {
51-
"type": "string",
52-
"examples": "example-bucket",
53-
},
54-
},
55-
"key": {
56-
"in": "body",
57-
"required": True,
58-
"description": "asset key",
59-
"schema": {
60-
"type": "string",
61-
"examples": "path/to/example/asset.xyz",
51+
# auth:schemes
52+
# ---
53+
# A property that contains all of the scheme definitions used by Assets and
54+
# Links in the STAC Item or Collection.
55+
# - Catalogs
56+
# - Collections
57+
# - Item Properties
58+
# "auth:schemes": {
59+
# "oauth": {
60+
# "type": "oauth2",
61+
# "description": "requires a login and user token",
62+
# "flows": {
63+
# "authorizationUrl": "https://example.com/oauth/authorize",
64+
# "tokenUrl": "https://example.com/oauth/token",
65+
# "scopes": {}
66+
# }
67+
# }
68+
# }
69+
# TODO: Add directly to Collections & Catalogs doc
70+
if "properties" in doc:
71+
schemes = doc["properties"].setdefault("auth:schemes", {})
72+
schemes[self.auth_scheme] = {
73+
"type": "oauth2",
74+
"description": "requires a login and user token",
75+
"flows": {
76+
# TODO: Get authorizationUrl and tokenUrl from config
77+
"authorizationCode": {
78+
"authorizationUrl": "https://example.com/oauth/authorize",
79+
"tokenUrl": "https://example.com/oauth/token",
80+
"scopes": {},
81+
},
82+
},
83+
}
84+
if self.signing_endpoint:
85+
schemes[self.signing_scheme] = {
86+
"type": "signedUrl",
87+
"description": "Requires an authentication API",
88+
"flows": {
89+
"authorizationCode": {
90+
"authorizationApi": self.signing_endpoint,
91+
"method": "POST",
92+
"parameters": {
93+
"bucket": {
94+
"in": "body",
95+
"required": True,
96+
"description": "asset bucket",
97+
"schema": {
98+
"type": "string",
99+
"examples": "example-bucket",
100+
},
101+
},
102+
"key": {
103+
"in": "body",
104+
"required": True,
105+
"description": "asset key",
106+
"schema": {
107+
"type": "string",
108+
"examples": "path/to/example/asset.xyz",
109+
},
110+
},
62111
},
63-
},
112+
"responseField": "signed_url",
113+
}
64114
},
65-
"responseField": "signed_url",
66115
}
67-
},
68-
}
69116

70-
for asset in item["assets"].values():
71-
if re.match(self.asset_expression, asset.get("href", "")):
72-
asset.setdefault("auth:refs", []).append(scheme)
73-
return item
117+
# auth:refs
118+
# ---
119+
# Annotate assets with "auth:refs": [signing_scheme]
120+
if self.signing_endpoint:
121+
for asset in doc.get("assets", {}).values():
122+
if "href" not in asset:
123+
logger.warning("Asset %s has no href", asset)
124+
continue
125+
if re.match(self.signed_asset_expression, asset["href"]):
126+
asset.setdefault("auth:refs", []).append(self.signing_scheme)
127+
128+
# Annotate links with "auth:refs": [auth_scheme]
129+
links = chain(
130+
doc.get("links", []),
131+
(
132+
link
133+
for prop in ["features", "collections"]
134+
for object_with_links in doc.get(prop, [])
135+
for link in object_with_links.get("links", [])
136+
),
137+
)
138+
for link in links:
139+
print(f"{link['href']=!s}")
140+
if "href" not in link:
141+
logger.warning("Link %s has no href", link)
142+
continue
143+
match = find_match(
144+
path=urlparse(link["href"]).path,
145+
method="GET",
146+
private_endpoints=self.private_endpoints,
147+
public_endpoints=self.public_endpoints,
148+
default_public=self.default_public,
149+
)
150+
if match.is_private:
151+
link.setdefault("auth:refs", []).append(self.auth_scheme)
152+
153+
return doc

0 commit comments

Comments
 (0)