Skip to content

Commit fbc6446

Browse files
GitHKAndrei Neagu
and
Andrei Neagu
authored
🎨 Adds authentication for new style dynamic services and platform vendor services ⚠️ (#6484)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent 515278a commit fbc6446

File tree

17 files changed

+271
-11
lines changed

17 files changed

+271
-11
lines changed

.env-devel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ STORAGE_PROFILING=1
220220

221221
SWARM_STACK_NAME=master-simcore
222222

223+
## VENDOR DEVELOPMENT SERVICES ---
224+
VENDOR_DEV_MANUAL_IMAGE=containous/whoami
225+
VENDOR_DEV_MANUAL_REPLICAS=1
226+
VENDOR_DEV_MANUAL_SUBDOMAIN=manual
227+
228+
## VENDOR DEVELOPMENT SERVICES ---
229+
223230
WB_API_WEBSERVER_HOST=wb-api-server
224231
WB_API_WEBSERVER_PORT=8080
225232

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ CPU_COUNT = $(shell cat /proc/cpuinfo | grep processor | wc -l )
269269
services/docker-compose.local.yml \
270270
> $@
271271

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

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

289294

290295

291-
.PHONY: up-devel up-prod up-prod-ci up-version up-latest .deploy-ops
296+
.PHONY: up-devel up-prod up-prod-ci up-version up-latest .deploy-ops .deploy-vendors
297+
298+
.deploy-vendors: .stack-vendor-services.yml
299+
# Deploy stack 'vendors'
300+
docker stack deploy --detach=true --with-registry-auth -c $< vendors
292301

293302
.deploy-ops: .stack-ops.yml
294303
# Deploy stack 'ops'
@@ -338,6 +347,7 @@ up-devel: .stack-simcore-development.yml .init-swarm $(CLIENT_WEB_OUTPUT) ## Dep
338347
@$(MAKE_C) services/dask-sidecar certificates
339348
# Deploy stack $(SWARM_STACK_NAME) [back-end]
340349
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
350+
@$(MAKE) .deploy-vendors
341351
@$(MAKE) .deploy-ops
342352
@$(_show_endpoints)
343353
@$(MAKE_C) services/static-webserver/client follow-dev-logs
@@ -348,6 +358,7 @@ up-devel-frontend: .stack-simcore-development-frontend.yml .init-swarm ## Every
348358
@$(MAKE_C) services/dask-sidecar certificates
349359
# Deploy stack $(SWARM_STACK_NAME) [back-end]
350360
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
361+
@$(MAKE) .deploy-vendors
351362
@$(MAKE) .deploy-ops
352363
@$(_show_endpoints)
353364
@$(MAKE_C) services/static-webserver/client follow-dev-logs
@@ -358,6 +369,7 @@ ifeq ($(target),)
358369
@$(MAKE_C) services/dask-sidecar certificates
359370
# Deploy stack $(SWARM_STACK_NAME)
360371
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
372+
@$(MAKE) .deploy-vendors
361373
@$(MAKE) .deploy-ops
362374
else
363375
# deploys ONLY $(target) service
@@ -369,6 +381,7 @@ up-version: .stack-simcore-version.yml .init-swarm ## Deploys versioned stack '$
369381
@$(MAKE_C) services/dask-sidecar certificates
370382
# Deploy stack $(SWARM_STACK_NAME)
371383
@docker stack deploy --detach=true --with-registry-auth -c $< $(SWARM_STACK_NAME)
384+
@$(MAKE) .deploy-vendors
372385
@$(MAKE) .deploy-ops
373386
@$(_show_endpoints)
374387

api/specs/web-server/_auth.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,21 @@ async def logout(_body: LogoutBody):
155155
"""user logout"""
156156

157157

158+
@router.get(
159+
"/auth:check",
160+
operation_id="check_authentication",
161+
status_code=status.HTTP_204_NO_CONTENT,
162+
responses={
163+
status.HTTP_401_UNAUTHORIZED: {
164+
"model": Envelope[Error],
165+
"description": "unauthorized reset due to invalid token code",
166+
}
167+
},
168+
)
169+
async def check_auth():
170+
"""checks if user is authenticated in the platform"""
171+
172+
158173
@router.post(
159174
"/auth/reset-password",
160175
response_model=Envelope[Log],
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
import pytest
5+
6+
from .helpers.docker import run_docker_compose_config
7+
8+
9+
@pytest.fixture(scope="module")
10+
def dev_vendors_docker_compose(
11+
osparc_simcore_root_dir: Path,
12+
osparc_simcore_scripts_dir: Path,
13+
env_file_for_testing: Path,
14+
temp_folder: Path,
15+
) -> dict[str, Any]:
16+
docker_compose_path = (
17+
osparc_simcore_root_dir / "services" / "docker-compose-dev-vendors.yml"
18+
)
19+
assert docker_compose_path.exists()
20+
21+
return run_docker_compose_config(
22+
project_dir=osparc_simcore_root_dir / "services",
23+
scripts_dir=osparc_simcore_scripts_dir,
24+
docker_compose_paths=docker_compose_path,
25+
env_file_path=env_file_for_testing,
26+
destination_path=temp_folder / "ops_docker_compose.yml",
27+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
from typing import Final
3+
4+
from settings_library.utils_session import DEFAULT_SESSION_COOKIE_NAME
5+
6+
pytest_plugins = [
7+
"pytest_simcore.dev_vendors_compose",
8+
"pytest_simcore.docker_compose",
9+
"pytest_simcore.repository_paths",
10+
]
11+
12+
13+
_SERVICE_TO_MIDDLEWARE_MAPPING: Final[dict[str, str]] = {
14+
"manual": "pytest-simcore_manual-auth"
15+
}
16+
17+
18+
def test_dev_vendors_docker_compose_auth_enabled(
19+
dev_vendors_docker_compose: dict[str, str]
20+
):
21+
22+
assert isinstance(dev_vendors_docker_compose["services"], dict)
23+
for service_name, service_spec in dev_vendors_docker_compose["services"].items():
24+
print(
25+
f"Checking vendor service '{service_name}'\n{json.dumps(service_spec, indent=2)}"
26+
)
27+
labels = service_spec["deploy"]["labels"]
28+
29+
# NOTE: when adding a new service it should also be added to the mapping
30+
auth_middleware_name = _SERVICE_TO_MIDDLEWARE_MAPPING[service_name]
31+
32+
prefix = f"traefik.http.middlewares.{auth_middleware_name}.forwardauth"
33+
34+
assert labels[f"{prefix}.trustForwardHeader"] == "true"
35+
assert "http://webserver:8080/v0/auth:check" in labels[f"{prefix}.address"]
36+
assert DEFAULT_SESSION_COOKIE_NAME in labels[f"{prefix}.authResponseHeaders"]
37+
assert (
38+
auth_middleware_name
39+
in labels["traefik.http.routers.pytest-simcore_manual.middlewares"]
40+
)

services/director-v2/src/simcore_service_director_v2/core/dynamic_services_settings/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pydantic import Field
22
from settings_library.base import BaseCustomSettings
3+
from settings_library.webserver import WebServerSettings
34

45
from .egress_proxy import EgressProxySettings
56
from .proxy import DynamicSidecarProxySettings
@@ -29,3 +30,5 @@ class DynamicServicesSettings(BaseCustomSettings):
2930
DYNAMIC_SIDECAR_PLACEMENT_SETTINGS: PlacementSettings = Field(
3031
auto_default_from_env=True
3132
)
33+
34+
WEBSERVER_SETTINGS: WebServerSettings = Field(auto_default_from_env=True)

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
)
1010
from pydantic import ByteSize
1111
from servicelib.common_headers import X_SIMCORE_USER_AGENT
12+
from settings_library import webserver
13+
from settings_library.utils_session import DEFAULT_SESSION_COOKIE_NAME
1214

1315
from ....core.dynamic_services_settings import DynamicServicesSettings
1416
from ....core.dynamic_services_settings.proxy import DynamicSidecarProxySettings
@@ -43,6 +45,9 @@ def get_dynamic_proxy_spec(
4345
dynamic_services_scheduler_settings: DynamicServicesSchedulerSettings = (
4446
dynamic_services_settings.DYNAMIC_SCHEDULER
4547
)
48+
webserver_settings: webserver.WebServerSettings = (
49+
dynamic_services_settings.WEBSERVER_SETTINGS
50+
)
4651

4752
mounts = [
4853
# docker socket needed to use the docker api
@@ -77,9 +82,11 @@ def get_dynamic_proxy_spec(
7782
"io.simcore.zone": f"{dynamic_services_scheduler_settings.TRAEFIK_SIMCORE_ZONE}",
7883
"traefik.docker.network": swarm_network_name,
7984
"traefik.enable": "true",
85+
# security
86+
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowcredentials": "true",
8087
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}",
8188
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowmethods": "GET,OPTIONS,PUT,POST,DELETE,PATCH,HEAD",
82-
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowheaders": f"{X_SIMCORE_USER_AGENT}",
89+
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolallowheaders": f"{X_SIMCORE_USER_AGENT},Set-Cookie",
8390
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accessControlAllowOriginList": ",".join(
8491
[
8592
f"{scheduler_data.request_scheme}://{scheduler_data.request_dns}",
@@ -88,11 +95,22 @@ def get_dynamic_proxy_spec(
8895
),
8996
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.accesscontrolmaxage": "100",
9097
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-security-headers.headers.addvaryheader": "true",
98+
# auth
99+
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-auth.forwardauth.address": f"{webserver_settings.api_base_url}/auth:check",
100+
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-auth.forwardauth.trustForwardHeader": "true",
101+
f"traefik.http.middlewares.{scheduler_data.proxy_service_name}-auth.forwardauth.authResponseHeaders": f"Set-Cookie,{DEFAULT_SESSION_COOKIE_NAME}",
102+
# routing
91103
f"traefik.http.services.{scheduler_data.proxy_service_name}.loadbalancer.server.port": "80",
92104
f"traefik.http.routers.{scheduler_data.proxy_service_name}.entrypoints": "http",
93105
f"traefik.http.routers.{scheduler_data.proxy_service_name}.priority": "10",
94106
f"traefik.http.routers.{scheduler_data.proxy_service_name}.rule": rf"HostRegexp(`{scheduler_data.node_uuid}\.services\.(?P<host>.+)`)",
95-
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",
107+
f"traefik.http.routers.{scheduler_data.proxy_service_name}.middlewares": ",".join(
108+
[
109+
f"{dynamic_services_scheduler_settings.SWARM_STACK_NAME}_gzip@swarm",
110+
f"{scheduler_data.proxy_service_name}-security-headers",
111+
f"{scheduler_data.proxy_service_name}-auth",
112+
]
113+
),
96114
"dynamic_type": "dynamic-sidecar", # tagged as dynamic service
97115
}
98116
| StandardSimcoreDockerLabels(
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
# NOTE: this stack is only for development and testing of vendor services.
3+
# the actualy code is deployed inside the ops repository.
4+
5+
services:
6+
7+
manual:
8+
image: ${VENDOR_DEV_MANUAL_IMAGE}
9+
init: true
10+
hostname: "{{.Node.Hostname}}-{{.Task.Slot}}"
11+
deploy:
12+
replicas: ${VENDOR_DEV_MANUAL_REPLICAS}
13+
labels:
14+
- io.simcore.zone=${TRAEFIK_SIMCORE_ZONE}
15+
- traefik.enable=true
16+
- traefik.docker.network=${SWARM_STACK_NAME}_default
17+
# auth
18+
- traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.address=http://${WEBSERVER_HOST}:${WEBSERVER_PORT}/v0/auth:check
19+
- traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.trustForwardHeader=true
20+
- traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.authResponseHeaders=Set-Cookie,osparc-sc
21+
# routing
22+
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.server.port=80
23+
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.healthcheck.path=/
24+
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.healthcheck.interval=2000ms
25+
- traefik.http.services.${SWARM_STACK_NAME}_manual.loadbalancer.healthcheck.timeout=1000ms
26+
- traefik.http.routers.${SWARM_STACK_NAME}_manual.entrypoints=http
27+
- traefik.http.routers.${SWARM_STACK_NAME}_manual.priority=10
28+
- traefik.http.routers.${SWARM_STACK_NAME}_manual.rule=HostRegexp(`${VENDOR_DEV_MANUAL_SUBDOMAIN}\.(?P<host>.+)`)
29+
- traefik.http.routers.${SWARM_STACK_NAME}_manual.middlewares=${SWARM_STACK_NAME}_gzip@swarm, ${SWARM_STACK_NAME}_manual-auth
30+
networks:
31+
- simcore_default
32+
33+
networks:
34+
simcore_default:
35+
name: ${SWARM_STACK_NAME}_default
36+
external: true

services/docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,9 @@ services:
379379
TRAEFIK_SIMCORE_ZONE: ${TRAEFIK_SIMCORE_ZONE}
380380
TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT: ${TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT}
381381
TRACING_OPENTELEMETRY_COLLECTOR_PORT: ${TRACING_OPENTELEMETRY_COLLECTOR_PORT}
382+
383+
WEBSERVER_HOST: ${WEBSERVER_HOST}
384+
WEBSERVER_PORT: ${WEBSERVER_PORT}
382385
volumes:
383386
- "/var/run/docker.sock:/var/run/docker.sock"
384387
deploy:

services/static-webserver/client/source/class/osparc/data/model/IframeHandler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ qx.Class.define("osparc.data.model.IframeHandler", {
329329
if (osparc.utils.Utils.isDevelopmentPlatform()) {
330330
console.log("Connecting: about to fetch", srvUrl);
331331
}
332-
fetch(srvUrl)
332+
fetch(srvUrl, {credentials: "include"})
333333
.then(response => {
334334
if (osparc.utils.Utils.isDevelopmentPlatform()) {
335335
console.log("Connecting: fetch's response status", response.status);

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,22 @@ paths:
233233
application/json:
234234
schema:
235235
$ref: '#/components/schemas/Envelope_Log_'
236+
/v0/auth:check:
237+
get:
238+
tags:
239+
- auth
240+
summary: Check Auth
241+
description: checks if user is authenticated in the platform
242+
operationId: check_authentication
243+
responses:
244+
'204':
245+
description: Successful Response
246+
'401':
247+
description: unauthorized reset due to invalid token code
248+
content:
249+
application/json:
250+
schema:
251+
$ref: '#/components/schemas/Envelope_Error_'
236252
/v0/auth/reset-password:
237253
post:
238254
tags:
@@ -4315,7 +4331,7 @@ paths:
43154331
'403':
43164332
description: ProjectInvalidRightsError
43174333
'404':
4318-
description: UserDefaultWalletNotFoundError, ProjectNotFoundError
4334+
description: ProjectNotFoundError, UserDefaultWalletNotFoundError
43194335
'409':
43204336
description: ProjectTooManyProjectOpenedError
43214337
'422':

services/web/server/src/simcore_service_webserver/login/_auth_handlers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,19 @@ async def logout(request: web.Request) -> web.Response:
300300
await forget_identity(request, response)
301301

302302
return response
303+
304+
305+
@routes.get(f"/{API_VTAG}/auth:check", name="check_authentication")
306+
@login_required
307+
async def check_auth(request: web.Request) -> web.Response:
308+
# lightweight endpoint for checking if users are authenticated
309+
# used primarily by Traefik auth middleware to verify session cookies
310+
311+
# NOTE: for future development
312+
# if database access is added here, services like jupyter-math
313+
# which load a lot of resources will have a big performance hit
314+
# consider caching some properties required by this endpoint or rely on Redis
315+
316+
assert request # nosec
317+
318+
return web.json_response(status=status.HTTP_204_NO_CONTENT)

0 commit comments

Comments
 (0)