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

🎨 Adds authentication for new style dynamic services and platform vendor services ⚠️ #6484

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
37162be
added mock unauthenticated manual service
Oct 2, 2024
85733af
cookies
Oct 2, 2024
0b53e9f
exposed manual
Oct 2, 2024
61a396c
dynamic-sidecar is now authenticated
Oct 2, 2024
9aa2728
extended EncryptedCookieStorage
Oct 2, 2024
9fb2a08
Merge remote-tracking branch 'upstream/master' into pr-osparc-manual-…
Oct 2, 2024
4673a62
using lightweight auth check endpoint
Oct 2, 2024
eb94357
director-v2 reads webserver endpoint form env vars
Oct 2, 2024
4445b34
refactor
Oct 2, 2024
1252409
link with cookie name
Oct 2, 2024
c8e8152
readme
Oct 2, 2024
5d8e115
fixed failing tests
Oct 2, 2024
e0c3398
added test for /auth:check
Oct 2, 2024
4f472d5
added notes for the future
Oct 2, 2024
05d93d3
refactor
Oct 2, 2024
3a69ec9
Merge remote-tracking branch 'upstream/master' into pr-osparc-manual-…
Oct 2, 2024
fe002fd
simplify service declaration
Oct 2, 2024
20a4145
ignore manual
Oct 2, 2024
8507df7
moved to vendors stack
Oct 2, 2024
212af33
extracted env vars to configure manual service
Oct 2, 2024
e6642b7
remove
Oct 2, 2024
199814b
Merge remote-tracking branch 'upstream/master' into pr-osparc-manual-…
Oct 2, 2024
132845b
disable manual by default
Oct 3, 2024
9fabb8a
Merge remote-tracking branch 'upstream/master' into pr-osparc-manual-…
Oct 3, 2024
d4f8626
revert this change
Oct 3, 2024
b51e457
fix failing tests
Oct 3, 2024
8e35dfc
Merge remote-tracking branch 'upstream/master' into pr-osparc-manual-…
Oct 3, 2024
3f2c54c
revert
Oct 3, 2024
066cf32
Merge remote-tracking branch 'upstream/master' into pr-osparc-manual-…
Oct 4, 2024
54dcd5f
refactor placement of services in stacks
Oct 4, 2024
4b4ba17
string refenreces
Oct 4, 2024
626307e
rename
Oct 4, 2024
12d1360
refactor interface
Oct 7, 2024
ef6b17f
added new tests and fixtures
Oct 7, 2024
1eb0ab0
refactor
Oct 7, 2024
62e96c3
fixed broken tests
Oct 7, 2024
bbb5ec9
Merge branch 'master' into pr-osparc-manual-for-logged-in-users
GitHK Oct 8, 2024
7aa314f
fixed broken test
Oct 8, 2024
752a2a6
finally fixed test
Oct 8, 2024
cf4b23f
Merge branch 'pr-osparc-manual-for-logged-in-users' of github.com:Git…
Oct 8, 2024
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
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],
GitHK marked this conversation as resolved.
Show resolved Hide resolved
"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
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
GitHK marked this conversation as resolved.
Show resolved Hide resolved
# 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
Loading