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

Restructure the example #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ dmypy.json

# Pyre type checker
.pyre/
.vscode/
localstack/
package/
artifact.zip
29 changes: 29 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM python:3.9
RUN apt-get -y update \
&& apt-get install -y gettext \
# Cleanup apt cache
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

ENV POETRY_VERSION=1.3.2
ENV POETRY_HOME=/opt/poetry
RUN curl -sSL https://install.python-poetry.org | python && \
cd /usr/local/bin && \
ln -s /opt/poetry/bin/poetry && \
poetry config virtualenvs.create false

WORKDIR /app
COPY poetry.lock pyproject.toml README.md /app/

ARG INSTALL_DEV=false
RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi"

COPY src/lambda_saleor_app /app/src/lambda_saleor_app
RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install ; else poetry install --no-dev ; fi"

# COPY .flake8 alembic.ini docker-entrypoint.sh /app/

EXPOSE 8080
# ENTRYPOINT ["/bin/bash", "docker-entrypoint.sh"]

CMD python -m lambda_saleor_app
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,18 @@ Manifest URL is `https://<your API gateway URL>/manifest.json`
![image](https://user-images.githubusercontent.com/1754812/218416975-edd53df2-791b-4421-8302-f7b054220a51.png)


Now once you click to your app name in the Dashboard, you'll see content rendered by the `/app` endpoint (the one defined as `appUrl` in manifest).
Now once you click on your app name in the Dashboard, you'll see content rendered by the `/app` endpoint (the one defined as `appUrl` in the manifest).


# Development

Using Localstack you can set up a local parameter store mock. It's provided in the attached docker-compose.

The easiest way to work on your app is to use Docker and Docker Compose, the following app with run your application with uvicorn, run Localstack in the background and reload when you make changes to your code.

```
docker-compose run --rm --service-ports app
```


Happy coding!
Expand Down
10 changes: 6 additions & 4 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/bin/bash

set -e

# Cleanup
rm -rf package
rm artifact.zip
rm -rf package || true
rm artifact.zip || true
poetry build
poetry run pip install --upgrade -t package dist/*.whl
cd package ; zip -r ../artifact.zip . -x '*.pyc'
poetry run pip install --platform=manylinux2014_x86_64 --only-binary=:all: --upgrade -t package dist/*.whl
cd package ; zip -r ../artifact.zip . -x '*.pyc'
36 changes: 36 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: "3.8"

services:
app:
build:
context: .
dockerfile: Dockerfile
args:
INSTALL_DEV: "true"
command: python -m lambda_saleor_app
ports:
- "8000:8000"
volumes:
- ./src/lambda_saleor_app:/app/src/lambda_saleor_app
environment:
- SALEOR_DOMAIN=mcabra.eu.saleor.cloud
- SSM_ENDPOINT=http://localstack:4566
- SSM_REGION=eu-west-1
- SSM_AWS_ACCESS_KEY_ID=test
- SSM_AWS_SECRET_ACCESS_KEY=test
depends_on:
- "localstack"


localstack:
image: localstack/localstack
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
- "127.0.0.1:4510-4559:4510-4559" # external services port range
environment:
- DEBUG=${DEBUG-}
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- "./localstack:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
2 changes: 0 additions & 2 deletions lambda_saleor_app/config.py

This file was deleted.

21 changes: 0 additions & 21 deletions lambda_saleor_app/utils/parameter_store.py

This file was deleted.

789 changes: 730 additions & 59 deletions poetry.lock

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ version = "0.1.0"
description = ""
authors = ["Artur Smęt <[email protected]>"]
readme = "README.md"
packages = [{include = "lambda_saleor_app"}]
packages = [{include = "lambda_saleor_app", from = "src"}]

[tool.poetry.dependencies]
python = "^3.10"
python = "^3.9"
fastapi = "^0.89.1"
mangum = "^0.17.0"
structlog = "^22.3.0"

saleor-sdk-python = "^0.0.2"
boto3 = "^1.26.69"
httpx = "^0.23.3"

[tool.poetry.group.dev.dependencies]
uvicorn = "^0.20.0"
boto3 = "^1.26.69"
black = "^23.1.0"
ipdb = "^0.13.11"

[build-system]
requires = ["poetry-core"]
Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions src/lambda_saleor_app/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import uvicorn

if __name__ == "__main__":
uvicorn.run(
"lambda_saleor_app.app:app",
host="0.0.0.0",
port=8000,
log_level="info",
reload=True,
)
40 changes: 13 additions & 27 deletions lambda_saleor_app/main.py → src/lambda_saleor_app/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from fastapi import FastAPI, Request
from fastapi import FastAPI, Request, Depends
from mangum import Mangum

from lambda_saleor_app import config
from lambda_saleor_app.settings import settings
from lambda_saleor_app.utils import logging, parameter_store
from lambda_saleor_app.utils.obfuscation import obfuscate
from lambda_saleor_app.schemas import InstallAuthToken
from lambda_saleor_app.deps import get_host, verify_webhook_signature

logger = logging.logger

Expand All @@ -11,19 +14,12 @@


@app.get("/manifest.json")
async def manifest(request: Request):
async def manifest(request_host: str = Depends(get_host)):
"""
Endpoint used to establish permissions and webhook subscriptions
"""
# Fetches host from API Gateway
try:
request_host = request.scope["aws.event"]["requestContext"]["domainName"]
except KeyError:
# Fallback to Host header
request_host = request.headers["host"]

return {
"id": config.APP_ID,
"id": settings.app_id,
"version": "1.0.0",
"name": "My serverless App",
"about": "My serverless App for extending Saleor.",
Expand All @@ -48,27 +44,23 @@ async def manifest(request: Request):


@app.post("/register")
async def register(request: Request):
async def register(auth_token: InstallAuthToken):
"""
Endpoint that handles final step of App installation - persisting the Saleor Token
"""
body = await request.json()
logger.debug("Register request", body=body)
auth_token = body.get("auth_token")
if auth_token:
logger.info("Persisting Saleor Token")
parameter_store.write_to_ssm(key="SaleorAPIKey", value=auth_token)
logger.debug("Register request", body=obfuscate(auth_token.auth_token))
logger.info("Persisting Saleor Token")
parameter_store.write_to_ssm(key=settings.ssm_saleor_app_auth_token_key, value=auth_token.auth_token)
return "OK"


@app.post("/api/webhooks/order-event")
async def webhook(request: Request):
async def webhook(request: Request, __ = Depends(verify_webhook_signature)):
"""
Public endpoint for receiving a webhook from Saleor
"""
payload = await request.json()
logger.info("Webhook request", body=payload, headers=request.headers)
# TODO: Verify webhook signature using Saleor-Domain and Saleor-Signature headers
return payload


Expand All @@ -82,15 +74,9 @@ async def dashboard_app(request: Request):
"saleor_domain": request.query_params.get("domain"),
"saleor_api_url": request.query_params.get("SaleorApiUrl"),
"stored_token": parameter_store.get_from_ssm(
"SaleorAPIKey"
settings.ssm_saleor_app_auth_token_key
), # FIXME: Demo only, insecure
}


handler = Mangum(app)


if __name__ == "__main__":
import uvicorn

uvicorn.run("main:app", port=8000, log_level="info")
77 changes: 77 additions & 0 deletions src/lambda_saleor_app/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from fastapi import Request, Depends, Header
from saleor_sdk.crypto.utils import decode_webook_payload, decode_jwt
from saleor_sdk.crypto.exceptions import JWKSKeyMissing

from lambda_saleor_app.utils.jwks import get_jwkset


async def get_host(request: Request):
# Fetches host from API Gateway
try:
return request.scope["aws.event"]["requestContext"]["domainName"]
except KeyError:
# Fallback to Host header
return request.headers["host"]


async def get_saleor_event(saleor_event: str = Header(..., alias="Saleor-Event")):
return saleor_event


async def get_saleor_domain(saleor_domain: str = Header(..., alias="Saleor-Domain")):
# TODO: these will always be single tenant apps, we should make them sticky to only one Salor instance
# and validate here if the incoming domain is the one configured in settings.
return saleor_domain


async def get_saleor_signature(
saleor_signature: str = Header(..., alias="Saleor-Signature")
):
return saleor_signature



async def get_saleor_user(
saleor_domain: str = Depends(get_saleor_domain),
saleor_token: str = Header(..., alias="Saleor-Token"),
):
saleor_jwks = await get_jwkset()
max_attempts = 1

# TODO: fix DRY with verify_webhook_signature
while max_attempts:
try:
return decode_jwt(
jwt=saleor_token,
jwks=saleor_jwks,
)
except JWKSKeyMissing as exc:
if max_attempts:
saleor_jwks = await get_jwkset()
max_attempts -= 1
else:
raise


async def verify_webhook_signature(
request: Request,
saleor_domain: str = Depends(get_saleor_domain),
jws: str = Depends(get_saleor_signature),
):
saleor_jwks = await get_jwkset()

max_attempts = 1

while max_attempts:
try:
return decode_webook_payload(
jws=jws,
jwks=saleor_jwks,
webhook_payload=await request.body(),
)
except JWKSKeyMissing as exc:
if max_attempts:
saleor_jwks = await get_jwkset()
max_attempts -= 1
else:
raise
5 changes: 5 additions & 0 deletions src/lambda_saleor_app/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel


class InstallAuthToken(BaseModel):
auth_token: str
35 changes: 35 additions & 0 deletions src/lambda_saleor_app/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Optional

from pydantic import BaseSettings, validator


class Settings(BaseSettings):
"""
Container for runtime configuration of the application.
"""

debug: bool = False
app_id: str
saleor_domain: str
ssm_timeout: float = 0.5
ssm_secret_prefix: Optional[str]

ssm_saleor_jwks_key: str = "saleor_jwks"
ssm_saleor_app_auth_token_key: str = "saleor_app_auth_token"

# Optional - for localstack / local development
ssm_endpoint: Optional[str]
ssm_region: Optional[str]
ssm_aws_access_key_id: Optional[str]
ssm_aws_secret_access_key: Optional[str]

@validator("ssm_secret_prefix", always=True)
def set_ssm_secret_prefix(cls, value, values):
if value:
return value
return f"/{values['app_id']}"


settings = Settings(
app_id="demo.lambda.app",
)
Loading