Skip to content

Enable runtime CORS configuration via environment variables #341

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

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7c02860
feature/1 added runtime configuration of CORS
captaincoordinates Jan 25, 2022
602e7b5
feature/1 updated documentation
captaincoordinates Jan 26, 2022
d2bc362
feature/1 doc updates
captaincoordinates Jan 26, 2022
1e4edd1
feature/1 PR feedback and additional test
captaincoordinates Jan 27, 2022
15c0866
feature/1 removed unwanted async on test
captaincoordinates Jan 29, 2022
1a3c55b
feature/1 updated with PR feedback from stac-fastapi
captaincoordinates Feb 1, 2022
ee69347
feature/1 updated documentation
captaincoordinates Feb 1, 2022
939ae13
feature/1 updated documentation
captaincoordinates Feb 1, 2022
973c68c
Merge branch 'master' of https://github.com/stac-utils/stac-fastapi i…
captaincoordinates Feb 1, 2022
1007d52
feature/1 follow pydantic configuration standard
captaincoordinates Feb 2, 2022
9aeb28b
feature/1 fix docs build
captaincoordinates Feb 2, 2022
1d47323
Merge remote-tracking branch 'upstream/master' into feature/1
captaincoordinates Feb 18, 2022
3ff4d10
feature/1 add CORS tests to api tests
captaincoordinates Feb 18, 2022
6f99709
feature/1 removed unnecessary tests
captaincoordinates Feb 18, 2022
2569aee
feature/1 added runtime configuration of CORS
captaincoordinates Jan 25, 2022
4162933
feature/1 updated documentation
captaincoordinates Jan 26, 2022
8736d15
feature/1 PR feedback and additional test
captaincoordinates Jan 27, 2022
1cc8506
feature/1 removed unwanted async on test
captaincoordinates Jan 29, 2022
f0e69b5
feature/1 updated with PR feedback from stac-fastapi
captaincoordinates Feb 1, 2022
76f467c
feature/1 updated documentation
captaincoordinates Feb 1, 2022
0dc532f
feature/1 updated documentation
captaincoordinates Feb 1, 2022
205eb6f
feature/1 follow pydantic configuration standard
captaincoordinates Feb 2, 2022
bfffddd
feature/1 fix docs build
captaincoordinates Feb 2, 2022
a0fa5fc
feature/1 add CORS tests to api tests
captaincoordinates Feb 18, 2022
99fdd85
feature/1 removed unnecessary tests
captaincoordinates Feb 18, 2022
8699c34
Fix intermittent error while loading test data
moradology Apr 19, 2022
e23b37e
Merge branch 'master' of https://github.com/stac-utils/stac-fastapi i…
captaincoordinates Apr 28, 2022
a749a6a
Merge branch 'feature/1' of https://github.com/moradology/stac-fastap…
captaincoordinates Apr 28, 2022
b0e740e
Rename settings (#7)
moradology May 13, 2022
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,7 @@ docs/api/*
.envrc

# Virtualenv
venv
venv

# IDE
.vscode
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

### Added

* Added ability to configure CORS middleware via environment variables ([#341](https://github.com/stac-utils/stac-fastapi/pull/341))
* Add hook to allow adding dependencies to routes. ([#295](https://github.com/stac-utils/stac-fastapi/pull/295))
* Ability to POST an ItemCollection to the collections/{collectionId}/items route. ([#367](https://github.com/stac-utils/stac-fastapi/pull/367))
* Add STAC API - Collections conformance class. ([383](https://github.com/stac-utils/stac-fastapi/pull/383))
* Added ability to configure CORS middleware via JSON configuration file and environment variable, rather than having to modify code. ([341](https://github.com/stac-utils/stac-fastapi/pull/341))
* Added ability to configure CORS middleware via environment variables ([#341](https://github.com/stac-utils/stac-fastapi/pull/341))

### Changed

Expand Down
16 changes: 10 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,33 @@ docker-run-all:
docker-compose up

.PHONY: docker-run-sqlalchemy
docker-run-sqlalchemy: image
docker-run-sqlalchemy: image run-joplin-sqlalchemy
$(run_sqlalchemy)

.PHONY: docker-run-pgstac
docker-run-pgstac: image
docker-run-pgstac: image run-joplin-pgstac
$(run_pgstac)

.PHONY: docker-shell-sqlalchemy
docker-shell-sqlalchemy:
docker-shell-sqlalchemy: run-joplin-sqlalchemy
$(run_sqlalchemy) /bin/bash

.PHONY: docker-shell-pgstac
docker-shell-pgstac:
docker-shell-pgstac: run-joplin-pgstac
$(run_pgstac) /bin/bash

.PHONY: test-sqlalchemy
test-sqlalchemy: run-joplin-sqlalchemy
$(run_sqlalchemy) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/sqlalchemy/tests/ && pytest -vvv'

.PHONY: test-pgstac
test-pgstac:
test-pgstac: run-joplin-pgstac
$(run_pgstac) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/pgstac/tests/ && pytest -vvv'

.PHONY: test-api
test-api:
docker-compose run api-tester

.PHONY: run-database
run-database:
docker-compose run --rm database
Expand All @@ -59,7 +63,7 @@ run-joplin-pgstac:
docker-compose run --rm loadjoplin-pgstac

.PHONY: test
test: test-sqlalchemy test-pgstac
test: test-sqlalchemy test-pgstac test-api

.PHONY: pybase-install
pybase-install:
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ services:
- database
- app-pgstac

api-tester:
image: stac-utils/stac-fastapi
build:
context: .
dockerfile: Dockerfile
profiles:
# prevent tester from starting with `docker-compose up`
- api-test
working_dir: /app/stac_fastapi/api
volumes:
- ./:/app
command: pytest -svvv

networks:
default:
name: stac-fastapi-network
13 changes: 4 additions & 9 deletions docs/tips-and-tricks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@ This page contains a few 'tips and tricks' for getting stac-fastapi working in v
CORS (Cross-Origin Resource Sharing) support may be required to use stac-fastapi in certain situations. For example, if you are running
[stac-browser](https://github.com/radiantearth/stac-browser) to browse the STAC catalog created by stac-fastapi, then you will need to enable CORS support.

To do this, edit `stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py` (or the equivalent in the `pgstac` folder) and add the following import:

To do this, configure environment variables for the configuration options described in [FastAPI docs](https://fastapi.tiangolo.com/tutorial/cors/) using a `cors_` prefix e.g.
```
from fastapi.middleware.cors import CORSMiddleware
cors_allow_credentials=true [or 1]
```

and then edit the `api = StacApi(...` call to add the following parameter:

Sequences, such as `allow_origins`, should be in JSON format e.g.
```
middlewares=[lambda app: CORSMiddleware(app, allow_origins=["*"])]
cors_allow_origins='["http://domain.one", "http://domain.two"]'
```

If needed, you can edit the `allow_origins` parameter to only allow CORS requests from specific origins.

## Enable the Context extension
The Context STAC extension provides information on the number of items matched and returned from a STAC search. This is required by various other STAC-related tools, such as the pystac command-line client. To enable the extension, edit `stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py` (or the equivalent in the `pgstac` folder) and add the following import:

Expand Down
5 changes: 4 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from starlette.responses import JSONResponse, Response

from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
from stac_fastapi.api.middleware import CORSMiddleware
from stac_fastapi.api.models import (
APIRequest,
CollectionUri,
Expand Down Expand Up @@ -91,7 +92,9 @@ class StacApi:
)
pagination_extension = attr.ib(default=TokenPaginationExtension)
response_class: Type[Response] = attr.ib(default=JSONResponse)
middlewares: List = attr.ib(default=attr.Factory(lambda: [BrotliMiddleware]))
middlewares: List = attr.ib(
default=attr.Factory(lambda: [BrotliMiddleware, CORSMiddleware])
)
route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attr.ib(default=[])

def get_extension(self, extension: Type[ApiExtension]) -> Optional[ApiExtension]:
Expand Down
21 changes: 21 additions & 0 deletions stac_fastapi/api/stac_fastapi/api/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""Application settings."""
import enum
from logging import getLogger
from typing import Final, Sequence

from pydantic import BaseSettings, Field

logger: Final = getLogger(__file__)


# TODO: Move to stac-pydantic
Expand All @@ -22,3 +28,18 @@ class AddOns(enum.Enum):
"""Enumeration of available third party add ons."""

bulk_transaction = "bulk-transaction"


class FastApiAppSettings(BaseSettings):
"""API settings."""

allow_origins: Sequence[str] = Field(("*",), env="cors_allow_origins")
allow_methods: Sequence[str] = Field(("*",), env="cors_allow_methods")
allow_headers: Sequence[str] = Field(("*",), env="cors_allow_headers")
allow_credentials: bool = Field(False, env="cors_allow_credentials")
allow_origin_regex: str = Field(None, env="cors_allow_origin_regex")
expose_headers: Sequence[str] = Field(("*",), env="cors_expose_headers")
max_age: int = Field(600, env="cors_max_age")


fastapi_app_settings: Final = FastApiAppSettings()
82 changes: 81 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/middleware.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
"""api middleware."""

from typing import Callable
from logging import getLogger
from typing import Callable, Final, Optional, Sequence

from fastapi import APIRouter, FastAPI
from fastapi.middleware import cors
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.routing import Match
from starlette.types import ASGIApp

from stac_fastapi.api.config import fastapi_app_settings

logger: Final = getLogger(__file__)


def router_middleware(app: FastAPI, router: APIRouter):
Expand All @@ -29,3 +36,76 @@ async def _middleware(request: Request, call_next):
return func

return deco


class CORSMiddleware(cors.CORSMiddleware):
"""Starlette CORS Middleware with configuration."""

def __init__(
self,
app: ASGIApp,
allow_origins: Optional[Sequence[str]] = None,
allow_methods: Optional[Sequence[str]] = None,
allow_headers: Optional[Sequence[str]] = None,
allow_credentials: Optional[bool] = None,
allow_origin_regex: Optional[str] = None,
expose_headers: Optional[Sequence[str]] = None,
max_age: Optional[int] = None,
) -> None:
"""Create CORSMiddleware Object."""
allow_origins = (
fastapi_app_settings.allow_origins
if allow_origins is None
else allow_origins
)
allow_methods = (
fastapi_app_settings.allow_methods
if allow_methods is None
else allow_methods
)
allow_headers = (
fastapi_app_settings.allow_headers
if allow_headers is None
else allow_headers
)
allow_credentials = (
fastapi_app_settings.allow_credentials
if allow_credentials is None
else allow_credentials
)
allow_origin_regex = (
fastapi_app_settings.allow_origin_regex
if allow_origin_regex is None
else allow_origin_regex
)
if allow_origin_regex is not None:
logger.info("allow_origin_regex present and will override allow_origins")
allow_origins = ""
expose_headers = (
fastapi_app_settings.expose_headers
if expose_headers is None
else expose_headers
)
max_age = fastapi_app_settings.max_age if max_age is None else max_age
logger.debug(
f"""
CORS configuration
allow_origins: {allow_origins}
allow_methods: {allow_methods}
allow_headers: {allow_headers}
allow_credentials: {allow_credentials}
allow_origin_regex: {allow_origin_regex}
expose_headers: {expose_headers}
max_age: {max_age}
"""
)
super().__init__(
app,
allow_origins=allow_origins,
allow_methods=allow_methods,
allow_headers=allow_headers,
allow_credentials=allow_credentials,
allow_origin_regex=allow_origin_regex,
expose_headers=expose_headers,
max_age=max_age,
)
Empty file.
60 changes: 60 additions & 0 deletions stac_fastapi/api/tests/cors_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from copy import deepcopy
from json import dumps
from typing import Final

from stac_fastapi.api.config import fastapi_app_settings

settings_fallback = deepcopy(fastapi_app_settings)
cors_origin_1: Final = "http://permit.one"
cors_origin_2: Final = "http://permit.two"
cors_origin_3: Final = "http://permit.three"
cors_origin_deny: Final = "http://deny.me"


def cors_permit_1():
fastapi_app_settings.allow_origins = dumps((cors_origin_1,))


def cors_permit_2():
fastapi_app_settings.allow_origins = dumps((cors_origin_2,))


def cors_permit_3():
fastapi_app_settings.allow_origins = dumps((cors_origin_3,))


def cors_permit_12():
fastapi_app_settings.allow_origins = dumps((cors_origin_1, cors_origin_2))


def cors_permit_123_regex():
fastapi_app_settings.allow_origin_regex = "http\\://permit\\..+"


def cors_deny():
fastapi_app_settings.allow_origins = dumps((cors_origin_deny,))


def cors_disable_get():
fastapi_app_settings.allow_methods = dumps(
(
"HEAD",
"POST",
"PUT",
"DELETE",
"CONNECT",
"OPTIONS",
"TRACE",
"PATCH",
)
)


def cors_clear_config():
fastapi_app_settings.allow_origins = settings_fallback.allow_origins
fastapi_app_settings.allow_methods = settings_fallback.allow_methods
fastapi_app_settings.allow_headers = settings_fallback.allow_headers
fastapi_app_settings.allow_credentials = settings_fallback.allow_credentials
fastapi_app_settings.allow_origin_regex = settings_fallback.allow_origin_regex
fastapi_app_settings.expose_headers = settings_fallback.expose_headers
fastapi_app_settings.max_age = settings_fallback.max_age
Loading