Skip to content

Commit

Permalink
Add CORS support (#164)
Browse files Browse the repository at this point in the history
* Add CORS support

* Update CORS

* Update fixture name

* Simplify CORS config values
  • Loading branch information
anayden authored Apr 1, 2020
1 parent b119009 commit 2b1f085
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ spec:
value: {{ .Values.NP_CLUSTER_NAME }}
- name: NP_MONITORING_K8S_KUBELET_PORT
value: {{ .Values.NP_MONITORING_K8S_KUBELET_PORT | quote }}
- name: NP_CORS_ORIGINS
value: {{ .Values.NP_CORS_ORIGINS }}
{{- if .Values.DOCKER_LOGIN_ARTIFACTORY_SECRET_NAME }}
imagePullSecrets:
- name: {{ .Values.DOCKER_LOGIN_ARTIFACTORY_SECRET_NAME }}
Expand Down
1 change: 1 addition & 0 deletions deploy/platformmonitoringapi/values-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ NP_MONITORING_K8S_NS: default
NP_MONITORING_REGISTRY_URL: https://registry-dev.neu.ro
NP_CLUSTER_NAME: "default"
NP_MONITORING_K8S_KUBELET_PORT: ""
NP_CORS_ORIGINS: http://localhost:8000,https://master--neuro-web.netlify.com
1 change: 1 addition & 0 deletions deploy/platformmonitoringapi/values-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ NP_MONITORING_K8S_NS: default
NP_MONITORING_REGISTRY_URL: https://registry-staging.neu.ro
NP_CLUSTER_NAME: ""
NP_MONITORING_K8S_KUBELET_PORT: ""
NP_CORS_ORIGINS: https://release--neuro-web.netlify.com,https://app.neu.ro
20 changes: 20 additions & 0 deletions platform_monitoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import aiohttp
import aiohttp.web
import aiohttp_cors
from aioelasticsearch import Elasticsearch
from aiohttp.web import (
HTTPBadRequest,
Expand All @@ -32,6 +33,7 @@
from .base import JobStats, Telemetry
from .config import (
Config,
CORSConfig,
ElasticsearchConfig,
KubeConfig,
PlatformApiConfig,
Expand Down Expand Up @@ -355,6 +357,22 @@ async def create_elasticsearch_client(
yield client


def _setup_cors(app: aiohttp.web.Application, config: CORSConfig) -> None:
if not config.allowed_origins:
return

logger.info(f"Setting up CORS with allowed origins: {config.allowed_origins}")
default_options = aiohttp_cors.ResourceOptions(
allow_credentials=True, expose_headers="*", allow_headers="*",
)
cors = aiohttp_cors.setup(
app, defaults={origin: default_options for origin in config.allowed_origins}
)
for route in app.router.routes():
logger.debug(f"Setting up CORS for {route}")
cors.add(route)


async def create_app(config: Config) -> aiohttp.web.Application:
app = aiohttp.web.Application(middlewares=[handle_exceptions])
app["config"] = config
Expand Down Expand Up @@ -407,6 +425,8 @@ async def _init_app(app: aiohttp.web.Application) -> AsyncIterator[None]:
api_v1_app.add_subapp("/jobs", monitoring_app)

app.add_subapp("/api/v1", api_v1_app)

_setup_cors(app, config.cors)
return app


Expand Down
6 changes: 6 additions & 0 deletions platform_monitoring/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class KubeClientAuthType(str, enum.Enum):
CERTIFICATE = "certificate"


@dataclass(frozen=True)
class CORSConfig:
allowed_origins: Sequence[str] = ()


@dataclass(frozen=True)
class KubeConfig:
endpoint_url: str
Expand Down Expand Up @@ -76,4 +81,5 @@ class Config:
kube: KubeConfig
docker: DockerConfig
registry: RegistryConfig
cors: CORSConfig
cluster_name: str
11 changes: 10 additions & 1 deletion platform_monitoring/config_factory.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import logging
import os
from pathlib import Path
from typing import Dict, Optional
from typing import Dict, Optional, Sequence

from yarl import URL

from .config import (
Config,
CORSConfig,
DockerConfig,
ElasticsearchConfig,
KubeClientAuthType,
Expand Down Expand Up @@ -36,6 +37,7 @@ def create(self) -> Config:
registry=self._create_registry(),
docker=self._create_docker(),
cluster_name=cluster_name,
cors=self.create_cors(),
)

def _create_server(self) -> ServerConfig:
Expand Down Expand Up @@ -101,3 +103,10 @@ def _create_registry(self) -> RegistryConfig:

def _create_docker(self) -> DockerConfig:
return DockerConfig()

def create_cors(self) -> CORSConfig:
origins: Sequence[str] = CORSConfig.allowed_origins
origins_str = self._environ.get("NP_CORS_ORIGINS", "").strip()
if origins_str:
origins = origins_str.split(",")
return CORSConfig(allowed_origins=origins)
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ ignore_missing_imports = true

[mypy-trafaret]
ignore_missing_imports = true

[mypy-aiohttp_cors]
ignore_missing_imports = true
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"docker-image-py==0.1.10",
"trafaret==2.0.2",
"platform-logging==0.3",
"aiohttp-cors==0.7.0",
)

setup(
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from platform_monitoring.config import (
Config,
CORSConfig,
DockerConfig,
ElasticsearchConfig,
KubeConfig,
Expand Down Expand Up @@ -164,6 +165,7 @@ def _f(**kwargs: Any) -> Config:
registry=registry_config,
docker=docker_config,
cluster_name=cluster_name,
cors=CORSConfig(allowed_origins=["https://neu.ro"]),
)
kwargs = {**defaults, **kwargs}
return Config(**kwargs)
Expand Down
66 changes: 66 additions & 0 deletions tests/integration/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,72 @@ async def test_secured_ping_non_existing_token_unauthorized(
async with client.get(url, headers=headers) as resp:
assert resp.status == HTTPUnauthorized.status_code

@pytest.mark.asyncio
async def test_ping_unknown_origin(
self, monitoring_api: MonitoringApiEndpoints, client: aiohttp.ClientSession
) -> None:
async with client.get(
monitoring_api.ping_url, headers={"Origin": "http://unknown"}
) as response:
assert response.status == HTTPOk.status_code, await response.text()
assert "Access-Control-Allow-Origin" not in response.headers

@pytest.mark.asyncio
async def test_ping_allowed_origin(
self, monitoring_api: MonitoringApiEndpoints, client: aiohttp.ClientSession
) -> None:
async with client.get(
monitoring_api.ping_url, headers={"Origin": "https://neu.ro"}
) as resp:
assert resp.status == HTTPOk.status_code, await resp.text()
assert resp.headers["Access-Control-Allow-Origin"] == "https://neu.ro"
assert resp.headers["Access-Control-Allow-Credentials"] == "true"
assert resp.headers["Access-Control-Expose-Headers"] == ""

@pytest.mark.asyncio
async def test_ping_options_no_headers(
self, monitoring_api: MonitoringApiEndpoints, client: aiohttp.ClientSession
) -> None:
async with client.options(monitoring_api.ping_url) as resp:
assert resp.status == HTTPForbidden.status_code, await resp.text()
assert await resp.text() == (
"CORS preflight request failed: "
"origin header is not specified in the request"
)

@pytest.mark.asyncio
async def test_ping_options_unknown_origin(
self, monitoring_api: MonitoringApiEndpoints, client: aiohttp.ClientSession
) -> None:
async with client.options(
monitoring_api.ping_url,
headers={
"Origin": "http://unknown",
"Access-Control-Request-Method": "GET",
},
) as resp:
assert resp.status == HTTPForbidden.status_code, await resp.text()
assert await resp.text() == (
"CORS preflight request failed: "
"origin 'http://unknown' is not allowed"
)

@pytest.mark.asyncio
async def test_ping_options(
self, monitoring_api: MonitoringApiEndpoints, client: aiohttp.ClientSession
) -> None:
async with client.options(
monitoring_api.ping_url,
headers={
"Origin": "https://neu.ro",
"Access-Control-Request-Method": "GET",
},
) as resp:
assert resp.status == HTTPOk.status_code, await resp.text()
assert resp.headers["Access-Control-Allow-Origin"] == "https://neu.ro"
assert resp.headers["Access-Control-Allow-Credentials"] == "true"
assert resp.headers["Access-Control-Allow-Methods"] == "GET"


class TestTopApi:
@pytest.mark.asyncio
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest
from platform_monitoring.config import (
Config,
CORSConfig,
DockerConfig,
ElasticsearchConfig,
KubeClientAuthType,
Expand Down Expand Up @@ -59,6 +60,7 @@ def test_create(cert_authority_path: str, token_path: str) -> None:
"NP_MONITORING_REGISTRY_URL": "http://testhost:5000",
"NP_CLUSTER_NAME": "default",
"NP_MONITORING_K8S_KUBELET_PORT": "12321",
"NP_CORS_ORIGINS": "https://domain1.com,http://do.main",
}
config = EnvironConfigFactory(environ).create()
assert config == Config(
Expand Down Expand Up @@ -86,6 +88,7 @@ def test_create(cert_authority_path: str, token_path: str) -> None:
registry=RegistryConfig(url=URL("http://testhost:5000")),
docker=DockerConfig(),
cluster_name="default",
cors=CORSConfig(["https://domain1.com", "http://do.main"]),
)


Expand Down

0 comments on commit 2b1f085

Please sign in to comment.