Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support transactions with validation and authentication #378

Merged
merged 63 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
0c4ea31
Add `TransactionExtension` to STAC API
slesaad May 20, 2024
ab8ae77
Add workflow for deployment on push to branch
slesaad May 20, 2024
6866ff8
Add auth to stac API
slesaad May 20, 2024
aadf943
Update local dockerfiles
slesaad May 20, 2024
9eb26ce
Skip test for now
slesaad May 21, 2024
4e8b4aa
Fix dockerfile copy
slesaad May 21, 2024
868c8fb
Add missing env vars to lambda
slesaad May 21, 2024
2d5a97e
Fix auth import
slesaad May 21, 2024
4499085
Fix various issues - missing env vars, folders, etc
slesaad May 21, 2024
6e62e73
Add __init__.py to common
slesaad May 21, 2024
786ca9d
Copy common folder properly
slesaad May 21, 2024
20faa30
Refactor auth.py
slesaad May 21, 2024
8660d7b
Add missing authentication to PUT endpoints
slesaad May 22, 2024
b80d232
Add validation and bulk transactions
slesaad May 22, 2024
10c1cb7
Remove stac-pydantic dependency
slesaad May 22, 2024
647a07c
Build jwks url during runtime
slesaad May 22, 2024
02b22f7
Fix stage env var
slesaad May 22, 2024
5b1e2be
Update bulkitem model
slesaad May 22, 2024
fe403c6
Remove duplication boto3 install
slesaad May 22, 2024
f2f0c40
Add log
slesaad May 22, 2024
795dabf
Prepend `root_path` to url for validation
slesaad May 22, 2024
bd3a79b
Handle validation error properly
slesaad May 22, 2024
5c7245e
Add docstrings
slesaad May 22, 2024
4f10ed9
Sort
slesaad May 22, 2024
ebaca03
Make linter happy
slesaad May 22, 2024
7c547ea
Sort imports
slesaad May 22, 2024
680b39c
Merge branch 'develop' into full-transaction-support
slesaad May 28, 2024
76b0b46
Merge branch 'develop' into full-transaction-support
slesaad Jun 17, 2024
3376bb1
Update variable name
slesaad Jun 17, 2024
bd6f72f
Revert `jwks_url` to build at infrastructure
slesaad Jun 17, 2024
ffe92c5
Add feature flag for enabling transactions
slesaad Jul 23, 2024
c40a5ca
Use `pystac` for validation instead
slesaad Jul 23, 2024
e6278d8
Add pystac in setup
slesaad Jul 23, 2024
16e56c0
Add package files
slesaad Jul 24, 2024
2afe9c4
Fix veda_auth package, use it in stac and ingest
slesaad Jul 24, 2024
0563818
Fix linting errors
slesaad Jul 24, 2024
bdb70f3
Make auth optional, fix item reference in bulk ingest
slesaad Jul 24, 2024
a45f18a
Set `jwks_url` only if `userpool_id` is provided
slesaad Jul 24, 2024
5e45496
Format
slesaad Jul 24, 2024
5befeb1
Pass `enable_transactions` env var as string not bool
slesaad Jul 24, 2024
5a1eac0
Install auth before running ingest tests
slesaad Jul 24, 2024
8d202c1
Format fix :facepalm:
slesaad Jul 24, 2024
15e0286
Rename folder names
slesaad Jul 24, 2024
84c8273
Merge branch 'develop' into full-transaction-support
slesaad Jul 24, 2024
76a8132
Ceil pydantic<2
slesaad Jul 24, 2024
a2da159
Use pydantic v2
slesaad Jul 24, 2024
dd47158
Fix pin pydantic<2
slesaad Jul 25, 2024
7b885f8
Use relevant path for dockerfile
slesaad Jul 25, 2024
b106531
Match fastapi requirements
slesaad Jul 25, 2024
224de7b
Add tests for transactions
slesaad Jul 25, 2024
35efc69
Try to fix dependency issues
slesaad Jul 25, 2024
d041398
Add docstrings
slesaad Jul 25, 2024
a4337dd
Add brotli to requirements
slesaad Jul 25, 2024
5eedb56
Change order of pip installs in stac_api
slesaad Jul 26, 2024
7d11f07
Format
slesaad Jul 26, 2024
85ed997
Remove temporary workflow
slesaad Jul 26, 2024
dbcaed7
Add missing env vars and fix authentication override
slesaad Jul 30, 2024
95e76b0
Format
slesaad Jul 30, 2024
23cb5ce
format
anayeaye Jul 31, 2024
99225ad
test transactions on pr
anayeaye Jul 31, 2024
e2b1f76
Update env var to have `VEDA_STAC_` prefix
slesaad Jul 31, 2024
87ba938
Comment out stac api unit tests for now
slesaad Aug 5, 2024
88dd1fb
Merge branch 'develop' into full-transaction-support
slesaad Aug 5, 2024
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
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ jobs:
- name: Install reqs for ingest api
run: python -m pip install -r ingest_api/runtime/requirements_dev.txt

- name: Install veda auth for ingest api
run: python -m pip install common/auth

- name: Ingest unit tests
run: NO_PYDANTIC_SSM_SETTINGS=1 python -m pytest ingest_api/runtime/tests/ -vv -s

Expand Down
1 change: 1 addition & 0 deletions common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""common utils shared by veda stacks"""
17 changes: 17 additions & 0 deletions common/auth/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Setup veda_auth
"""

from setuptools import find_packages, setup

inst_reqs = ["cryptography>=42.0.5", "pyjwt>=2.8.0", "fastapi", "pydantic<2"]

setup(
name="veda_auth",
version="0.0.1",
description="",
python_requires=">=3.7",
packages=find_packages(),
zip_safe=False,
install_requires=inst_reqs,
include_package_data=True,
)
5 changes: 5 additions & 0 deletions common/auth/veda_auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
VEDA cognito auth
"""

from veda_auth.main import VedaAuth # noqa: F401
128 changes: 128 additions & 0 deletions common/auth/veda_auth/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Authentication handler for veda.stac and veda.ingest"""

import base64
import hashlib
import hmac
import logging
from typing import Annotated, Any, Dict

import boto3
import jwt

from fastapi import Depends, HTTPException, Security, security, status

logger = logging.getLogger(__name__)


class VedaAuth:
"""Class for handling authentication"""

def __init__(self, settings) -> None:
"""
Args:
settings: pydantic settings object containing cognito details
Returns:
None

"""
self.oauth2_scheme = security.OAuth2AuthorizationCodeBearer(
authorizationUrl=settings.cognito_authorization_url,
tokenUrl=settings.cognito_token_url,
refreshUrl=settings.cognito_token_url,
)

self.jwks_client = jwt.PyJWKClient(settings.jwks_url) # Caches JWKS

def validated_token(
token_str: Annotated[str, Security(self.oauth2_scheme)],
required_scopes: security.SecurityScopes,
) -> Dict:
# Parse & validate token
logger.info(f"\nToken String {token_str}")
try:
token = jwt.decode(
token_str,
self.jwks_client.get_signing_key_from_jwt(token_str).key,
algorithms=["RS256"],
)
except jwt.exceptions.InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
) from e

# Validate scopes (if required)
for scope in required_scopes.scopes:
if scope not in token["scope"]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not enough permissions",
headers={
"WWW-Authenticate": f'Bearer scope="{required_scopes.scope_str}"'
},
)

return token

self.validated_token = validated_token

def get_username(
token: Annotated[Dict[Any, Any], Depends(self.validated_token)]
) -> str:
result = token["username"] if "username" in token else str(token.get("sub"))
return result

self.get_username = get_username

def _get_secret_hash(
self, username: str, client_id: str, client_secret: str
) -> str:
# A keyed-hash message authentication code (HMAC) calculated using
# the secret key of a user pool client and username plus the client
# ID in the message.
message = username + client_id
dig = hmac.new(
bytearray(client_secret, "utf-8"),
msg=message.encode("UTF-8"),
digestmod=hashlib.sha256,
).digest()
return base64.b64encode(dig).decode()

def authenticate_and_get_token(
self,
username: str,
password: str,
user_pool_id: str,
app_client_id: str,
app_client_secret: str,
) -> Dict:
"""Authenticates the credentials and returns token"""
client = boto3.client("cognito-idp")
if app_client_secret:
auth_params = {
"USERNAME": username,
"PASSWORD": password,
"SECRET_HASH": self._get_secret_hash(
username, app_client_id, app_client_secret
),
}
else:
auth_params = {
"USERNAME": username,
"PASSWORD": password,
}
try:
resp = client.admin_initiate_auth(
UserPoolId=user_pool_id,
ClientId=app_client_id,
AuthFlow="ADMIN_USER_PASSWORD_AUTH",
AuthParameters=auth_params,
)
except client.exceptions.NotAuthorizedException:
return {
"message": "Login failed, please make sure the credentials are correct."
}
except Exception as e:
return {"message": f"Login failed with exception {e}"}
return resp["AuthenticationResult"]
6 changes: 0 additions & 6 deletions ingest_api/infrastructure/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,6 @@ def __init__(
value=self.api.url,
)

register_ssm_parameter(
self,
name="jwks_url",
value=self.jwks_url,
description="JWKS URL for Cognito user pool",
)
register_ssm_parameter(
self,
name="dynamodb_table",
Expand Down
4 changes: 4 additions & 0 deletions ingest_api/runtime/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ FROM public.ecr.aws/sam/build-python3.9:latest

WORKDIR /tmp

COPY common/auth /tmp/common/auth
RUN pip install /tmp/common/auth -t /asset
RUN rm -rf /tmp/common

COPY ingest_api/runtime/requirements.txt /tmp/ingestor/requirements.txt
RUN pip install -r /tmp/ingestor/requirements.txt -t /asset --no-binary pydantic uvicorn
RUN rm -rf /tmp/ingestor
Expand Down
1 change: 0 additions & 1 deletion ingest_api/runtime/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ orjson>=3.6.8
psycopg[binary,pool]>=3.0.15
pydantic_ssm_settings>=0.2.0
pydantic>=1.10.12
pyjwt>=2.8.0
pypgstac==0.7.4
python-multipart==0.0.7
requests>=2.27.1
Expand Down
106 changes: 0 additions & 106 deletions ingest_api/runtime/src/auth.py

This file was deleted.

3 changes: 3 additions & 0 deletions ingest_api/runtime/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pydantic import AnyHttpUrl, BaseSettings, Field, constr
from pydantic_ssm_settings import AwsSsmSourceConfig
from veda_auth import VedaAuth

AwsArn = constr(regex=r"^arn:aws:iam::\d{12}:role/.+")

Expand Down Expand Up @@ -63,3 +64,5 @@ def from_ssm(cls, stack: str):
),
)
)

auth = VedaAuth(settings)
5 changes: 2 additions & 3 deletions ingest_api/runtime/src/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import logging

import boto3
import src.auth as auth
import src.config as config
import src.services as services
from src.config import auth, settings

from fastapi import Depends, HTTPException, security

Expand All @@ -14,7 +13,7 @@

def get_table():
client = boto3.resource("dynamodb")
return client.Table(config.settings.dynamodb_table)
return client.Table(settings.dynamodb_table)


def get_db(table=Depends(get_table)) -> services.Database:
Expand Down
3 changes: 1 addition & 2 deletions ingest_api/runtime/src/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from typing import Dict

import src.auth as auth
import src.dependencies as dependencies
import src.schemas as schemas
import src.services as services
from aws_lambda_powertools.metrics import MetricUnit
from src.collection_publisher import CollectionPublisher, ItemPublisher
from src.config import settings
from src.config import auth, settings
from src.doc import DESCRIPTION
from src.monitoring import LoggerRouteHandler, logger, metrics, tracer

Expand Down
2 changes: 2 additions & 0 deletions local/Dockerfile.ingest
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ RUN pip install -r /tmp/ingestor/requirements.txt --no-binary pydantic uvicorn
RUN rm -rf /tmp/ingestor
# TODO this is temporary until we use a real packaging system like setup.py or poetry
COPY ingest_api/runtime/src /asset/src
COPY common/auth /tmp/common/auth
RUN pip install /tmp/common/auth

# # Reduce package size and remove useless files
RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[2-3][0-9]//'); cp $f $n; done;
Expand Down
8 changes: 5 additions & 3 deletions local/Dockerfile.stac
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ FROM ghcr.io/vincentsarago/uvicorn-gunicorn:${PYTHON_VERSION}

ENV CURL_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt

RUN pip install boto3

COPY stac_api/runtime /tmp/stac
# Installing boto3, which isn't needed in the lambda container instance
# since lambda execution environment includes boto3 by default
RUN pip install boto3

COPY stac_api/runtime /tmp/stac

COPY common/auth /tmp/stac/common/auth
RUN pip install /tmp/stac/common/auth
RUN pip install /tmp/stac
RUN rm -rf /tmp/stac

Expand Down
Loading