Skip to content

Commit

Permalink
🎨 Adds authentication for new style dynamic services and platform ven…
Browse files Browse the repository at this point in the history
…dor services ⚠️ (#6484)

Co-authored-by: Andrei Neagu <[email protected]>
  • Loading branch information
GitHK and Andrei Neagu authored Oct 8, 2024
1 parent 515278a commit fbc6446
Show file tree
Hide file tree
Showing 17 changed files with 271 additions and 11 deletions.
7 changes: 7 additions & 0 deletions .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ STORAGE_PROFILING=1

SWARM_STACK_NAME=master-simcore

## VENDOR DEVELOPMENT SERVICES ---
VENDOR_DEV_MANUAL_IMAGE=containous/whoami
VENDOR_DEV_MANUAL_REPLICAS=1
VENDOR_DEV_MANUAL_SUBDOMAIN=manual

## VENDOR DEVELOPMENT SERVICES ---

WB_API_WEBSERVER_HOST=wb-api-server
WB_API_WEBSERVER_PORT=8080

Expand Down
15 changes: 14 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ CPU_COUNT = $(shell cat /proc/cpuinfo | grep processor | wc -l )
services/docker-compose.local.yml \
> $@

.stack-vendor-services.yml: .env $(docker-compose-configs)
# Creating config for vendors stack to $@
@scripts/docker/docker-stack-config.bash -e $< \
services/docker-compose-dev-vendors.yml \
> $@

.stack-ops.yml: .env $(docker-compose-configs)
# Creating config for ops stack to $@
Expand All @@ -288,7 +293,11 @@ endif



.PHONY: up-devel up-prod up-prod-ci up-version up-latest .deploy-ops
.PHONY: up-devel up-prod up-prod-ci up-version up-latest .deploy-ops .deploy-vendors

.deploy-vendors: .stack-vendor-services.yml
# Deploy stack 'vendors'
docker stack deploy --detach=true --with-registry-auth -c $< vendors

.deploy-ops: .stack-ops.yml
# Deploy stack 'ops'
Expand Down Expand Up @@ -338,6 +347,7 @@ up-devel: .stack-simcore-development.yml .init-swarm $(CLIENT_WEB_OUTPUT) ## Dep
@$(MAKE_C) services/dask-sidecar certificates
# Deploy stack $(SWARM_STACK_NAME) [back-end]
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
@$(MAKE) .deploy-vendors
@$(MAKE) .deploy-ops
@$(_show_endpoints)
@$(MAKE_C) services/static-webserver/client follow-dev-logs
Expand All @@ -348,6 +358,7 @@ up-devel-frontend: .stack-simcore-development-frontend.yml .init-swarm ## Every
@$(MAKE_C) services/dask-sidecar certificates
# Deploy stack $(SWARM_STACK_NAME) [back-end]
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
@$(MAKE) .deploy-vendors
@$(MAKE) .deploy-ops
@$(_show_endpoints)
@$(MAKE_C) services/static-webserver/client follow-dev-logs
Expand All @@ -358,6 +369,7 @@ ifeq ($(target),)
@$(MAKE_C) services/dask-sidecar certificates
# Deploy stack $(SWARM_STACK_NAME)
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
@$(MAKE) .deploy-vendors
@$(MAKE) .deploy-ops
else
# deploys ONLY $(target) service
Expand All @@ -369,6 +381,7 @@ up-version: .stack-simcore-version.yml .init-swarm ## Deploys versioned stack '$
@$(MAKE_C) services/dask-sidecar certificates
# Deploy stack $(SWARM_STACK_NAME)
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
@$(MAKE) .deploy-vendors
@$(MAKE) .deploy-ops
@$(_show_endpoints)

Expand Down
15 changes: 15 additions & 0 deletions api/specs/web-server/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ async def logout(_body: LogoutBody):
"""user logout"""


@router.get(
"/auth:check",
operation_id="check_authentication",
status_code=status.HTTP_204_NO_CONTENT,
responses={
status.HTTP_401_UNAUTHORIZED: {
"model": Envelope[Error],
"description": "unauthorized reset due to invalid token code",
}
},
)
async def check_auth():
"""checks if user is authenticated in the platform"""


@router.post(
"/auth/reset-password",
response_model=Envelope[Log],
Expand Down
27 changes: 27 additions & 0 deletions packages/pytest-simcore/src/pytest_simcore/dev_vendors_compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pathlib import Path
from typing import Any

import pytest

from .helpers.docker import run_docker_compose_config


@pytest.fixture(scope="module")
def dev_vendors_docker_compose(
osparc_simcore_root_dir: Path,
osparc_simcore_scripts_dir: Path,
env_file_for_testing: Path,
temp_folder: Path,
) -> dict[str, Any]:
docker_compose_path = (
osparc_simcore_root_dir / "services" / "docker-compose-dev-vendors.yml"
)
assert docker_compose_path.exists()

return run_docker_compose_config(
project_dir=osparc_simcore_root_dir / "services",
scripts_dir=osparc_simcore_scripts_dir,
docker_compose_paths=docker_compose_path,
env_file_path=env_file_for_testing,
destination_path=temp_folder / "ops_docker_compose.yml",
)
40 changes: 40 additions & 0 deletions packages/pytest-simcore/tests/test_dev_vendors_compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import json
from typing import Final

from settings_library.utils_session import DEFAULT_SESSION_COOKIE_NAME

pytest_plugins = [
"pytest_simcore.dev_vendors_compose",
"pytest_simcore.docker_compose",
"pytest_simcore.repository_paths",
]


_SERVICE_TO_MIDDLEWARE_MAPPING: Final[dict[str, str]] = {
"manual": "pytest-simcore_manual-auth"
}


def test_dev_vendors_docker_compose_auth_enabled(
dev_vendors_docker_compose: dict[str, str]
):

assert isinstance(dev_vendors_docker_compose["services"], dict)
for service_name, service_spec in dev_vendors_docker_compose["services"].items():
print(
f"Checking vendor service '{service_name}'\n{json.dumps(service_spec, indent=2)}"
)
labels = service_spec["deploy"]["labels"]

# NOTE: when adding a new service it should also be added to the mapping
auth_middleware_name = _SERVICE_TO_MIDDLEWARE_MAPPING[service_name]

prefix = f"traefik.http.middlewares.{auth_middleware_name}.forwardauth"

assert labels[f"{prefix}.trustForwardHeader"] == "true"
assert "http://webserver:8080/v0/auth:check" in labels[f"{prefix}.address"]
assert DEFAULT_SESSION_COOKIE_NAME in labels[f"{prefix}.authResponseHeaders"]
assert (
auth_middleware_name
in labels["traefik.http.routers.pytest-simcore_manual.middlewares"]
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pydantic import Field
from settings_library.base import BaseCustomSettings
from settings_library.webserver import WebServerSettings

from .egress_proxy import EgressProxySettings
from .proxy import DynamicSidecarProxySettings
Expand Down Expand Up @@ -29,3 +30,5 @@ class DynamicServicesSettings(BaseCustomSettings):
DYNAMIC_SIDECAR_PLACEMENT_SETTINGS: PlacementSettings = Field(
auto_default_from_env=True
)

WEBSERVER_SETTINGS: WebServerSettings = Field(auto_default_from_env=True)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
)
from pydantic import ByteSize
from servicelib.common_headers import X_SIMCORE_USER_AGENT
from settings_library import webserver
from settings_library.utils_session import DEFAULT_SESSION_COOKIE_NAME

from ....core.dynamic_services_settings import DynamicServicesSettings
from ....core.dynamic_services_settings.proxy import DynamicSidecarProxySettings
Expand Down Expand Up @@ -43,6 +45,9 @@ def get_dynamic_proxy_spec(
dynamic_services_scheduler_settings: DynamicServicesSchedulerSettings = (
dynamic_services_settings.DYNAMIC_SCHEDULER
)
webserver_settings: webserver.WebServerSettings = (
dynamic_services_settings.WEBSERVER_SETTINGS
)

mounts = [
# docker socket needed to use the docker api
Expand Down Expand Up @@ -77,9 +82,11 @@ def get_dynamic_proxy_spec(
"io.simcore.zone": f"{dynamic_services_scheduler_settings.TRAEFIK_SIMCORE_ZONE}",
"traefik.docker.network": swarm_network_name,
"traefik.enable": "true",
# security
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowcredentials": "true",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.customresponseheaders.Content-Security-Policy": f"frame-ancestors {scheduler_data.request_dns} {scheduler_data.node_uuid}.services.{scheduler_data.request_dns}",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowmethods": "GET,OPTIONS,PUT,POST,DELETE,PATCH,HEAD",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowheaders": f"{X_SIMCORE_USER_AGENT}",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowheaders": f"{X_SIMCORE_USER_AGENT},Set-Cookie",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accessControlAllowOriginList": ",".join(
[
f"{scheduler_data.request_scheme}://{scheduler_data.request_dns}",
Expand All @@ -88,11 +95,22 @@ def get_dynamic_proxy_spec(
),
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolmaxage": "100",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.addvaryheader": "true",
# auth
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-auth.forwardauth.address": f"{webserver_settings.api_base_url}/auth:check",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-auth.forwardauth.trustForwardHeader": "true",
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-auth.forwardauth.authResponseHeaders": f"Set-Cookie,{DEFAULT_SESSION_COOKIE_NAME}",
# routing
f"traefik.http.services.{scheduler_data.proxy_service_name}.loadbalancer.server.port": "80",
f"traefik.http.routers.{scheduler_data.proxy_service_name}.entrypoints": "http",
f"traefik.http.routers.{scheduler_data.proxy_service_name}.priority": "10",
f"traefik.http.routers.{scheduler_data.proxy_service_name}.rule": rf"HostRegexp(`{scheduler_data.node_uuid}\.services\.(?P<host>.+)`)",
f"traefik.http.routers.{scheduler_data.proxy_service_name}.middlewares": f"{dynamic_services_scheduler_settings.SWARM_STACK_NAME}_gzip@swarm, {scheduler_data.proxy_service_name}-security-headers",
f"traefik.http.routers.{scheduler_data.proxy_service_name}.middlewares": ",".join(
[
f"{dynamic_services_scheduler_settings.SWARM_STACK_NAME}_gzip@swarm",
f"{scheduler_data.proxy_service_name}-security-headers",
f"{scheduler_data.proxy_service_name}-auth",
]
),
"dynamic_type": "dynamic-sidecar", # tagged as dynamic service
}
| StandardSimcoreDockerLabels(
Expand Down
36 changes: 36 additions & 0 deletions services/docker-compose-dev-vendors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

# NOTE: this stack is only for development and testing of vendor services.
# the actualy code is deployed inside the ops repository.

services:

manual:
image: ${VENDOR_DEV_MANUAL_IMAGE}
init: true
hostname: "{{.Node.Hostname}}-{{.Task.Slot}}"
deploy:
replicas: ${VENDOR_DEV_MANUAL_REPLICAS}
labels:
- io.simcore.zone=${TRAEFIK_SIMCORE_ZONE}
- traefik.enable=true
- traefik.docker.network=${SWARM_STACK_NAME}_default
# auth
- traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.address=http://${WEBSERVER_HOST}:${WEBSERVER_PORT}/v0/auth:check
- traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.trustForwardHeader=true
- traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.authResponseHeaders=Set-Cookie,osparc-sc
# routing
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.server.port=80
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.healthcheck.path=/
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.healthcheck.interval=2000ms
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.healthcheck.timeout=1000ms
- traefik.http.routers.${SWARM_STACK_NAME}_manual.entrypoints=http
- traefik.http.routers.${SWARM_STACK_NAME}_manual.priority=10
- traefik.http.routers.${SWARM_STACK_NAME}_manual.rule=HostRegexp(`${VENDOR_DEV_MANUAL_SUBDOMAIN}\.(?P<host>.+)`)
- traefik.http.routers.${SWARM_STACK_NAME}_manual.middlewares=${SWARM_STACK_NAME}_gzip@swarm, ${SWARM_STACK_NAME}_manual-auth
networks:
- simcore_default

networks:
simcore_default:
name: ${SWARM_STACK_NAME}_default
external: true
3 changes: 3 additions & 0 deletions services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,9 @@ services:
TRAEFIK_SIMCORE_ZONE: ${TRAEFIK_SIMCORE_ZONE}
TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT: ${TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT}
TRACING_OPENTELEMETRY_COLLECTOR_PORT: ${TRACING_OPENTELEMETRY_COLLECTOR_PORT}

WEBSERVER_HOST: ${WEBSERVER_HOST}
WEBSERVER_PORT: ${WEBSERVER_PORT}
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ qx.Class.define("osparc.data.model.IframeHandler", {
if (osparc.utils.Utils.isDevelopmentPlatform()) {
console.log("Connecting: about to fetch", srvUrl);
}
fetch(srvUrl)
fetch(srvUrl, {credentials: "include"})
.then(response => {
if (osparc.utils.Utils.isDevelopmentPlatform()) {
console.log("Connecting: fetch's response status", response.status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,22 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Envelope_Log_'
/v0/auth:check:
get:
tags:
- auth
summary: Check Auth
description: checks if user is authenticated in the platform
operationId: check_authentication
responses:
'204':
description: Successful Response
'401':
description: unauthorized reset due to invalid token code
content:
application/json:
schema:
$ref: '#/components/schemas/Envelope_Error_'
/v0/auth/reset-password:
post:
tags:
Expand Down Expand Up @@ -4315,7 +4331,7 @@ paths:
'403':
description: ProjectInvalidRightsError
'404':
description: UserDefaultWalletNotFoundError, ProjectNotFoundError
description: ProjectNotFoundError, UserDefaultWalletNotFoundError
'409':
description: ProjectTooManyProjectOpenedError
'422':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,19 @@ async def logout(request: web.Request) -> web.Response:
await forget_identity(request, response)

return response


@routes.get(f"/{API_VTAG}/auth:check", name="check_authentication")
@login_required
async def check_auth(request: web.Request) -> web.Response:
# lightweight endpoint for checking if users are authenticated
# used primarily by Traefik auth middleware to verify session cookies

# NOTE: for future development
# if database access is added here, services like jupyter-math
# which load a lot of resources will have a big performance hit
# consider caching some properties required by this endpoint or rely on Redis

assert request # nosec

return web.json_response(status=status.HTTP_204_NO_CONTENT)
Loading

0 comments on commit fbc6446

Please sign in to comment.