Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 39 additions & 3 deletions docs/07-row_level_auth.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@
"proxy applies it to every relevant request — without touching stac-fastapi-pgstac at all.\n",
"\n",
"<div class=\"alert alert-block alert-warning\">\n",
"<b>Note:</b> This chapter needs the local docker-compose auth stack. It will not run against\n",
"the hosted workshop deployment.\n",
"<b>Which stack you need.</b> Sections 7.4&nbsp;&ndash;&nbsp;7.5.1 run against\n",
"<i>either</i> the local docker-compose stack or the hosted deployment &mdash; see\n",
"&sect;7.3.3 for the deployed variant. Section&nbsp;7.6 onward needs docker-compose,\n",
"because it creates and deletes records and the deployed STAC API has the transaction\n",
"extension disabled on purpose.\n",
"</div>"
]
},
Expand Down Expand Up @@ -218,7 +221,40 @@
"time: `private-alice-demo` and `private-bob-demo`. To try it, open the proxy's Swagger UI\n",
"at `https://{project}-protected-stac.eoapi.dev/api.html`, click **Authorize**, sign in as\n",
"`alice`, and call `GET /collections`: `private-alice-demo` is listed and\n",
"`private-bob-demo` is not. Signed out, neither appears."
"`private-bob-demo` is not. Signed out, neither appears.\n",
"\n",
"### Running the read-side against the deployment\n",
"\n",
"Sections 7.4 to 7.5.1 only read, so they work against the deployed proxy too. Swap the\n",
"setup cell in 7.4 for this, using the same workshop token `workshop_setup.setup()` asks\n",
"for:\n",
"\n",
"```python\n",
"from stac_auth import auth_headers, deployed_auth, get_cognito_token\n",
"\n",
"cfg = deployed_auth(workshop_token)\n",
"stac_api_endpoint = cfg[\"stac_endpoint\"]\n",
"\n",
"def token_for(user):\n",
" return get_cognito_token(user, cfg[\"password\"], cfg[\"client_id\"], cfg[\"region\"])\n",
"\n",
"alice, bob, anonymous = auth_headers(token_for(\"alice\")), auth_headers(token_for(\"bob\")), {}\n",
"```\n",
"\n",
"Then use the seeded collections in place of the ones 7.4 creates:\n",
"\n",
"```python\n",
"public_id = \"eoapi-workshop-sentinel-2-c1-l2a\"\n",
"alice_id, bob_id = \"private-alice-demo\", \"private-bob-demo\"\n",
"```\n",
"\n",
"and skip the item cells, since the seeded collections have no items. 7.5 and 7.5.1 then\n",
"run unchanged, and you stop before 7.6.\n",
"\n",
"Two things this shows that the local stack does not. The tokens are signed by a real\n",
"identity provider rather than a mock, and they carry no `stac/write` scope &mdash; so even\n",
"the write-rejection in 7.6 would fail for the *wrong* reason (route-level auth, not\n",
"row-level), which is precisely why that section stays local."
]
},
{
Expand Down
91 changes: 89 additions & 2 deletions docs/stac_auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""
Helpers for authenticated STAC transaction requests in the local docker-compose stack.
Helpers for authenticated STAC requests against the workshop's auth stack.

Requires MOCK_OIDC_ENDPOINT and STAC_API_ENDPOINT (stac-auth-proxy).
The local docker-compose stack is the default: `get_mock_oidc_token` needs
MOCK_OIDC_ENDPOINT and STAC_API_ENDPOINT (stac-auth-proxy).

`deployed_auth` and `get_cognito_token` are the hosted equivalents, for running the
read-side of chapter 7 against the deployed proxy without docker. They cannot do writes:
the deployed STAC API has the transaction extension disabled on purpose.
"""

from __future__ import annotations
Expand Down Expand Up @@ -78,3 +83,85 @@ def get_mock_oidc_token(

def auth_headers(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}


def deployed_auth(
workshop_token: str,
config_url: str | None = None,
timeout: float = 20.0,
) -> dict[str, str]:
"""Look up the deployed proxy endpoint and Cognito details.

Args:
workshop_token: the bearer token for the config endpoint, the same one
`workshop_setup.setup()` asks for.

Returns a dict with `stac_endpoint`, `client_id`, `region` and `password`.
"""
config_url = config_url or os.getenv(
"CONFIG_API_ENDPOINT", "https://workshop-config.eoapi.dev"
)
response = httpx.get(
config_url,
headers=auth_headers(workshop_token),
timeout=timeout,
)
response.raise_for_status()
config = response.json()

discovery = config.get("oidc_discovery_url", "")
# https://cognito-idp.<region>.amazonaws.com/<pool>/.well-known/...
region = discovery.split("://", 1)[-1].split(".")[1] if discovery else ""

missing = [
key
for key, value in {
"stac_auth_proxy_endpoint": config.get("stac_auth_proxy_endpoint"),
"oidc_client_id": config.get("oidc_client_id"),
}.items()
if not value
]
if missing:
raise RuntimeError(
f"The config endpoint did not return {missing}. "
"The deployment may predate the Cognito auth proxy."
)

return {
"stac_endpoint": config["stac_auth_proxy_endpoint"],
"client_id": config["oidc_client_id"],
"region": region,
"password": config.get("workshop_user_password", ""),
}


def get_cognito_token(
username: str,
password: str,
client_id: str,
region: str,
timeout: float = 20.0,
) -> str:
"""Return a Cognito access token for `username`, without a browser redirect.

Uses `USER_PASSWORD_AUTH`, which the workshop's app client enables for exactly this
reason. The resulting token carries the `username` claim the row-level filter reads,
but *not* the `stac/write` scope, so it can read the catalog and never write to it.
"""
response = httpx.post(
f"https://cognito-idp.{region}.amazonaws.com/",
headers={
"Content-Type": "application/x-amz-json-1.1",
"X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
},
json={
"AuthFlow": "USER_PASSWORD_AUTH",
"ClientId": client_id,
"AuthParameters": {"USERNAME": username, "PASSWORD": password},
},
timeout=timeout,
)
if response.status_code != 200:
raise RuntimeError(f"Cognito rejected the sign-in: {response.text}")

return response.json()["AuthenticationResult"]["AccessToken"]
13 changes: 13 additions & 0 deletions infrastructure/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,19 @@ def __init__(
"http://localhost:8086",
],
),
# Lets a notebook sign in as alice or bob without a browser redirect, so
# chapter 7's read-side demo can run against this deployment and not only
# against docker-compose.
#
# This does not weaken the write protection. Cognito issues
# USER_PASSWORD_AUTH tokens with the scope `aws.cognito.signin.user.admin`
# and never the custom `stac/*` scopes, so such a token still fails the
# scope check in PRIVATE_ENDPOINTS. It carries `username`, which is all the
# row-level filter reads.
#
# `user_srp` is kept because naming any flow replaces Cognito's implicit
# defaults, and dropping SRP would be an unintended narrowing.
auth_flows=aws_cognito.AuthFlow(user_password=True, user_srp=True),
)

# Two workshop identities sharing one password. Cognito creates users in
Expand Down