From 5b0532117ff0c1279ebc9207910e5e21a6c3a793 Mon Sep 17 00:00:00 2001 From: Alain Mazy Date: Tue, 29 Aug 2023 09:30:14 +0200 Subject: [PATCH 1/7] added OHIF_DATA_SOURCE env var defaulting to 'dicom-web' --- .../shares/orthanc_token_service.py | 26 ++++++++++++++----- .../shares/orthanc_token_service_factory.py | 12 ++++++++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/sources/orthanc_auth_service/shares/orthanc_token_service.py b/sources/orthanc_auth_service/shares/orthanc_token_service.py index e785db1..3d841a2 100644 --- a/sources/orthanc_auth_service/shares/orthanc_token_service.py +++ b/sources/orthanc_auth_service/shares/orthanc_token_service.py @@ -21,6 +21,7 @@ class OrthancTokenService: public_orthanc_root_: Optional[str] = None public_ohif_root_: Optional[str] = None server_id_: Optional[str] = None + ohif_data_source_: str = "dicom-web" meddream_token_service_url_: Optional[str] = None public_meddream_root_: Optional[str] = None @@ -39,10 +40,11 @@ def _configure_meddream(self, meddream_token_service_url: str, public_meddream_r self.public_meddream_root_ = public_meddream_root self.public_landing_root_ = public_landing_root - def _configure_ohif(self, public_ohif_root: str, server_id: Optional[str] = None, public_landing_root: Optional[str] = None): + def _configure_ohif(self, public_ohif_root: str, server_id: Optional[str] = None, public_landing_root: Optional[str] = None, ohif_data_source: str = "dicom-web"): self.public_ohif_root_ = public_ohif_root self.server_id_ = server_id self.public_landing_root_ = public_landing_root + self.ohif_data_source_ = ohif_data_source def _create(self): if not self.tokens_manager_: @@ -85,15 +87,27 @@ def _generate_url(self, request: TokenCreationRequest, token: str, skip_landing_ return urllib.parse.urljoin(self.public_landing_root_, f"?token={token}") elif request.type == TokenType.OHIF_VIEWER_PUBLICATION: - if not has_dicom_uids: - logging.error("No dicom_uid provided while generating a link to the OHIF viewer") + + if self.ohif_data_source_ == "dicom-json": + if not has_orthanc_ids: + logging.error("No orthanc_id provided while generating a link to the OHIF viewer in 'dicom-json' data source mode") + return None + studyIds = ",".join([s.orthanc_id for s in request.resources]) + ohif_url_format = f"viewer?url=../studies/{studyIds}/ohif-dicom-json&token={token}" + elif self.ohif_data_source_ == "dicom-web": + if not has_dicom_uids: + logging.error("No dicom_uid provided while generating a link to the OHIF viewer in 'dicom-web' data source mode") + return None + studyIds = ",".join([s.dicom_uid for s in request.resources]) + ohif_url_format = f"viewer?StudyInstanceUIDs={studyIds}&token={token}" + else: + logging.error(f"Unsupported OHIF data source: {self.ohif_data_source_}") return None + if skip_landing_page or self.public_landing_root_ is None: public_root = self.public_ohif_root_ - - studyIds = ",".join([s.dicom_uid for s in request.resources]) - return urllib.parse.urljoin(public_root, f"viewer?StudyInstanceUIDs={studyIds}&token={token}") + return urllib.parse.urljoin(public_root, ohif_url_format) else: return urllib.parse.urljoin(self.public_landing_root_, f"?token={token}") diff --git a/sources/orthanc_auth_service/shares/orthanc_token_service_factory.py b/sources/orthanc_auth_service/shares/orthanc_token_service_factory.py index a2ae8fa..17d45ed 100644 --- a/sources/orthanc_auth_service/shares/orthanc_token_service_factory.py +++ b/sources/orthanc_auth_service/shares/orthanc_token_service_factory.py @@ -50,6 +50,15 @@ def create_token_service_from_secrets(): else: server_id = get_secret_or_die("SERVER_ID") + if not is_secret_defined("OHIF_DATA_SOURCE"): + logging.warning("OHIF_DATA_SOURCE is not defined, will default to dicom-web.") + ohif_data_source = "dicom-web" + else: + ohif_data_source = get_secret_or_die("OHIF_DATA_SOURCE") + if not ohif_data_source in ["dicom-web", "dicom-json"]: + logging.warning("Invalid OHIF_DATA_SOURCE value. It should be either 'dicom-json' or 'dicom-web', defaulting to 'dicom-web'.") + ohif_data_source = "dicom-web" + if not is_secret_defined("PUBLIC_LANDING_ROOT"): logging.warning("PUBLIC_LANDING_ROOT is not defined. Users won't get a clear error message if their link is invalid or expired") else: @@ -58,7 +67,8 @@ def create_token_service_from_secrets(): token_service._configure_ohif( public_ohif_root=public_ohif_root, server_id=server_id, - public_landing_root=public_landing_root + public_landing_root=public_landing_root, + ohif_data_source=ohif_data_source ) else: logging.warning("PUBLIC_OHIF_ROOT is not defined, the generator will not allow 'ohif-viewer-publication'") From d28cd6ece87467fc2e4a9e96f89bb65db46b215e Mon Sep 17 00:00:00 2001 From: Alain Mazy Date: Tue, 12 Sep 2023 14:21:32 +0200 Subject: [PATCH 2/7] handling authorized_labels --- release-notes.md | 2 + sources/Dockerfile.orthanc-auth-service | 2 +- sources/orthanc_auth_service/app.py | 32 +++++---- sources/orthanc_auth_service/permissions.json | 14 +++- .../orthanc_auth_service/shares/keycloak.py | 65 +++++++++++++------ sources/orthanc_auth_service/shares/models.py | 24 ++----- .../shares/tokens_manager.py | 5 +- sources/requirements.txt | 2 +- 8 files changed, 91 insertions(+), 55 deletions(-) diff --git a/release-notes.md b/release-notes.md index 7167d79..25cea6d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,8 @@ SPDX-License-Identifier: GPL-3.0-or-later v x.x.x ======== +BREAKING CHANGES: +- the format of the permissions.json file has changed to include `permissions` and `authorized_labels`. - nginx: added proxy parameters to handle large headers diff --git a/sources/Dockerfile.orthanc-auth-service b/sources/Dockerfile.orthanc-auth-service index 2ca7faf..79ca2b8 100644 --- a/sources/Dockerfile.orthanc-auth-service +++ b/sources/Dockerfile.orthanc-auth-service @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: CC0-1.0 -FROM python:3.10 +FROM python:3.11 ENV PYTHONUNBUFFERED=1 diff --git a/sources/orthanc_auth_service/app.py b/sources/orthanc_auth_service/app.py index 8673e0e..743f9b8 100644 --- a/sources/orthanc_auth_service/app.py +++ b/sources/orthanc_auth_service/app.py @@ -33,6 +33,15 @@ security = None logging.warning("!!!! HTTP Basic auth is NOT required to connect to the web-service !!!!") +# to show invalid payloads (debug) +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + exc_str = f'{exc}'.replace('\n', ' ').replace(' ', ' ') + logging.error(f"{request}: {exc_str}") + content = {'status_code': 10422, 'message': exc_str, 'data': None} + return JSONResponse(content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) # callback that is used on every request to check the auth-service caller's credentials def authorize(credentials: HTTPBasicCredentials = Depends(security)): @@ -147,28 +156,27 @@ def decode_token(request: TokenDecoderRequest): def get_user_profile(user_profile_request: UserProfileRequest): logging.info("get user profile: " + user_profile_request.json()) - try: - if keycloak is None: - logging.warning("Keycloak is not configured, all users are considered anonymous") - response = UserProfileResponse( + anonymous_profile = UserProfileResponse( name="Anonymous", permissions=[], + authorized_labels=[], validity=60 ) + try: + if keycloak is None: + logging.warning("Keycloak is not configured, all users are considered anonymous") + return anonymous_profile elif user_profile_request.token_key is not None: response = keycloak.get_user_profile_from_token(user_profile_request.token_value) else: - response = UserProfileResponse( - name="Anonymous", - permissions=[], - validity=60 - ) + return anonymous_profile return response except jwt.exceptions.InvalidAlgorithmError: - raise HTTPException(status_code=400, detail=str("Not a user token")) + # not a valid user profile, consider it is anonymous + return anonymous_profile except jwt.exceptions.PyJWTError: raise HTTPException(status_code=400, detail=str("Unable to decode token")) - except: - raise HTTPException(status_code=400, detail=str("Unexpected error")) + except Exception as ex: + raise HTTPException(status_code=400, detail=str("Unexpected error: " + str(ex))) diff --git a/sources/orthanc_auth_service/permissions.json b/sources/orthanc_auth_service/permissions.json index 145163d..4acd4e5 100644 --- a/sources/orthanc_auth_service/permissions.json +++ b/sources/orthanc_auth_service/permissions.json @@ -1,6 +1,16 @@ { "roles" : { - "admin": ["all"], - "doctor": ["view", "download", "share", "send"] + "admin": { + "permissions": ["all"], + "authorized_labels": ["*"] + }, + "doctor": { + "permissions":["view", "download", "share", "send"], + "authorized_labels": ["*"] + }, + "students": { + "permissions":["view", "download"], + "authorized_labels": ["students"] + } } } \ No newline at end of file diff --git a/sources/orthanc_auth_service/shares/keycloak.py b/sources/orthanc_auth_service/shares/keycloak.py index 4c16eb7..2d241d9 100644 --- a/sources/orthanc_auth_service/shares/keycloak.py +++ b/sources/orthanc_auth_service/shares/keycloak.py @@ -7,14 +7,14 @@ import requests import jwt import json -from typing import Dict, Any, List +from typing import Dict, Any, List, Tuple from .models import * from .utils.utils import get_secret_or_die, is_secret_defined class Keycloak: - def __init__(self, public_key, configured_roles): + def __init__(self, public_key, configured_roles: Dict[str, Any]): self.public_key = public_key self.configured_roles = configured_roles @@ -84,33 +84,46 @@ def get_roles_from_decoded_token(self, decoded_token: Dict[str, Any]) -> List[st def get_user_profile_from_token(self, jwt_token: str) -> UserProfileResponse: decoded_token = self.decode_token(jwt_token=jwt_token) - response = UserProfileResponse(name="", permissions=[], validity=60) + response = UserProfileResponse(name="", permissions=[], validity=60, authorized_labels=[]) response.name = self.get_name_from_decoded_token(decoded_token=decoded_token) roles = self.get_roles_from_decoded_token(decoded_token=decoded_token) - response.permissions = self.get_permissions_from_roles(roles) + response.permissions, response.authorized_labels = self.get_role_configuration(roles) return response - def get_permissions_from_roles(self, roles: List[str]) -> List[UserPermissions]: - response = [] + def get_role_configuration(self, roles: List[str]) -> Tuple[List[UserPermissions], List[str], List[str]]: + permissions = [] + authorized_labels = [] + configured_user_roles = [] - # for each role received from the token sent by Keycloak - for role in roles: - # search for it in the configured roles - configured_role = self.configured_roles.get(role) - # if it has been configured: - if configured_role is not None: - # Let's add the permissions in the response - for item in configured_role: - # (if not already there) - if UserPermissions(item) not in response: - response.append(UserPermissions(item)) + for r in roles: + if r in self.configured_roles: + configured_user_roles.append(r) - return response + # complain if there are 2 roles for the same user ??? How should we combine the authorized and forbidden labels in this case ??? + if len(configured_user_roles) > 1: + raise ValueError("Unable to handle multiple roles for a single user") + + role = configured_user_roles[0] + # search for it in the configured roles + configured_role = self.configured_roles.get(role) + # if it has been configured: + if configured_role is None: + raise ValueError(f"Role not found in configuration: {role}") + + for item in configured_role.get('permissions'): + # (if not already there) + if UserPermissions(item) not in permissions: + permissions.append(UserPermissions(item)) + + if configured_role.get("authorized_labels"): + authorized_labels = configured_role.get("authorized_labels") + + return permissions, authorized_labels def _get_keycloak_public_key(keycloak_uri: str) -> str: @@ -131,7 +144,21 @@ def _get_keycloak_public_key(keycloak_uri: str) -> str: def _get_config_from_file(file_path: str): with open(file_path) as f: data = json.load(f) - return data.get('roles') + + roles = data.get('roles') + + for key, role_def in roles.items(): + if not role_def.get("authorized_labels"): + msg = f'No "authorized_labels" defined for role "{key}". You should, e.g, include "authorized_labels" = ["*"] if you want to authorize all labels.")' + logging.error(msg) + raise ValueError(msg) + + if not role_def.get("permissions"): + msg = f'No "permissions" defined for role "{key}". You should, e.g, include "permissions" = ["all"] if you want to authorize all permissions.")' + logging.error(msg) + raise ValueError(msg) + + return roles def create_keycloak_from_secrets(): diff --git a/sources/orthanc_auth_service/shares/models.py b/sources/orthanc_auth_service/shares/models.py index 2c10347..52b7cf3 100644 --- a/sources/orthanc_auth_service/shares/models.py +++ b/sources/orthanc_auth_service/shares/models.py @@ -4,22 +4,10 @@ from typing import Optional, List from pydantic import BaseModel, Field -from pydantic.datetime_parse import parse_datetime from enum import Enum from datetime import datetime -class StringDateTime(datetime): - @classmethod - def __get_validators__(cls): - yield parse_datetime - yield cls.validate - - @classmethod - def validate(cls, v: datetime): - return v.isoformat() - - class Levels(str, Enum): PATIENT = 'patient' STUDY = 'study' @@ -67,18 +55,18 @@ class OrthancResource(BaseModel): level: Levels class Config: # allow creating object from dict (used when deserializing the JWT) - allow_population_by_field_name = True + populate_by_name = True class TokenCreationRequest(BaseModel): id: Optional[str] = None resources: List[OrthancResource] type: TokenType = Field(default=TokenType.INVALID) - expiration_date: Optional[StringDateTime] = Field(alias="expiration-date", default=None) + expiration_date: Optional[datetime] = Field(alias="expiration-date", default=None) validity_duration: Optional[int] = Field(alias='validity-duration', default=None) # alternate way to provide an expiration_date, more convenient for instant-links since the duration is relative to the server time, not the client time ! class Config: # allow creating object from dict (used when deserializing the JWT) - allow_population_by_field_name = True + populate_by_name = True class TokenCreationResponse(BaseModel): @@ -95,7 +83,7 @@ class TokenValidationRequest(BaseModel): server_id: Optional[str] = Field(alias="server-id", default=None) level: Optional[Levels] method: Methods - uri: Optional[str] + uri: Optional[str] = None class TokenValidationResponse(BaseModel): @@ -139,8 +127,10 @@ class UserPermissions(str, Enum): class UserProfileResponse(BaseModel): name: str + authorized_labels: List[str] = Field(alias="authorized-labels", default_factory=list) permissions: List[UserPermissions] = Field(default_factory=list) validity: int class Config: - use_enum_values = True \ No newline at end of file + use_enum_values = True + populate_by_name = True \ No newline at end of file diff --git a/sources/orthanc_auth_service/shares/tokens_manager.py b/sources/orthanc_auth_service/shares/tokens_manager.py index f73e841..f4d7d58 100644 --- a/sources/orthanc_auth_service/shares/tokens_manager.py +++ b/sources/orthanc_auth_service/shares/tokens_manager.py @@ -33,10 +33,9 @@ def get_request_from_token(self, token: str) -> TokenCreationRequest: def is_expired(self, request: TokenCreationRequest) -> bool: # check expiration date if request.expiration_date: - expiration_date = parser.parse(request.expiration_date) now_utc = pytz.UTC.localize(datetime.now()) - is_valid = now_utc < expiration_date + is_valid = now_utc < request.expiration_date if not is_valid: logging.warning(f"Token Validation: period is invalid") return not is_valid @@ -54,7 +53,7 @@ def is_valid(self, token: str, orthanc_id: Optional[str] = None, dicom_uid: Opti try: r = self._decode_token(token) share_request = TokenCreationRequest(**r) - except: + except Exception as ex: logging.warning(f"Token Validation: failed to decode token") return False diff --git a/sources/requirements.txt b/sources/requirements.txt index b83e5a9..08c8d99 100644 --- a/sources/requirements.txt +++ b/sources/requirements.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later pyjwt[crypto]==2.6.0 -fastapi==0.78.0 +fastapi==0.103.0 jinja2==3.1.2 pytz==2022.1 python-dateutil==2.8.2 From b0bc97371654a2326901df01f1187af825315e7d Mon Sep 17 00:00:00 2001 From: Alain Mazy Date: Tue, 12 Sep 2023 14:53:21 +0200 Subject: [PATCH 3/7] username in profile --- sources/orthanc_auth_service/shares/keycloak.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/sources/orthanc_auth_service/shares/keycloak.py b/sources/orthanc_auth_service/shares/keycloak.py index 2d241d9..1ddda7f 100644 --- a/sources/orthanc_auth_service/shares/keycloak.py +++ b/sources/orthanc_auth_service/shares/keycloak.py @@ -22,9 +22,12 @@ def decode_token(self, jwt_token: str) -> Dict[str, Any]: return jwt.decode(jwt=jwt_token, key=self.public_key, audience="account", algorithms=["RS256"]) def get_name_from_decoded_token(self, decoded_token: Dict[str, Any]) -> str: - name = decoded_token.get('name') - if name is not None: - return name + if decoded_token.get('name'): + return decoded_token.get('name') + + if decoded_token.get("preferred_username"): + return decoded_token.get("preferred_username") + return '' def get_roles_from_decoded_token(self, decoded_token: Dict[str, Any]) -> List[str]: @@ -84,9 +87,11 @@ def get_roles_from_decoded_token(self, decoded_token: Dict[str, Any]) -> List[st def get_user_profile_from_token(self, jwt_token: str) -> UserProfileResponse: decoded_token = self.decode_token(jwt_token=jwt_token) - response = UserProfileResponse(name="", permissions=[], validity=60, authorized_labels=[]) - - response.name = self.get_name_from_decoded_token(decoded_token=decoded_token) + response = UserProfileResponse( + name=self.get_name_from_decoded_token(decoded_token=decoded_token), + permissions=[], + validity=60, + authorized_labels=[]) roles = self.get_roles_from_decoded_token(decoded_token=decoded_token) From b6196693a450e448ee415695c540ca7c06c7e423 Mon Sep 17 00:00:00 2001 From: Alain Mazy Date: Tue, 12 Sep 2023 15:02:33 +0200 Subject: [PATCH 4/7] updated version of sample setups --- minimal-setup/basic-auth/docker-compose.yml | 8 ++++---- minimal-setup/keycloak/docker-compose.yml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/minimal-setup/basic-auth/docker-compose.yml b/minimal-setup/basic-auth/docker-compose.yml index 29e6737..14804d4 100644 --- a/minimal-setup/basic-auth/docker-compose.yml +++ b/minimal-setup/basic-auth/docker-compose.yml @@ -6,7 +6,7 @@ version: "3" services: nginx: - image: orthancteam/orthanc-nginx:23.5.0 + image: orthancteam/orthanc-nginx:23.6.1 depends_on: [orthanc, orthanc-auth-service, orthanc-for-shares] restart: unless-stopped ports: ["80:80"] @@ -22,7 +22,7 @@ services: ENABLE_HTTPS: "false" orthanc: - image: osimis/orthanc:23.4.0 + image: osimis/orthanc:current volumes: - orthanc-storage:/var/lib/orthanc/db depends_on: [orthanc-db] @@ -60,7 +60,7 @@ services: } orthanc-for-shares: - image: osimis/orthanc:23.5.0 + image: osimis/orthanc:current volumes: - orthanc-storage:/var/lib/orthanc/db depends_on: [orthanc-db] @@ -102,7 +102,7 @@ services: } orthanc-auth-service: - image: orthancteam/orthanc-auth-service:23.5.0 + image: orthancteam/orthanc-auth-service:labels-perm restart: unless-stopped environment: SECRET_KEY: "change-me-I-am-a-secret-key" diff --git a/minimal-setup/keycloak/docker-compose.yml b/minimal-setup/keycloak/docker-compose.yml index 0e97f79..f9b6cc7 100644 --- a/minimal-setup/keycloak/docker-compose.yml +++ b/minimal-setup/keycloak/docker-compose.yml @@ -23,7 +23,7 @@ services: ENABLE_OHIF: "true" orthanc: - image: osimis/orthanc:master-unstable + image: osimis/orthanc:current volumes: - orthanc-storage:/var/lib/orthanc/db depends_on: [orthanc-db] @@ -76,7 +76,7 @@ services: } orthanc-auth-service: - image: orthancteam/orthanc-auth-service:23.6.1 + image: orthancteam/orthanc-auth-service:current depends_on: [keycloak] restart: unless-stopped environment: @@ -106,7 +106,7 @@ services: keycloak: - image: orthancteam/orthanc-keycloak:23.6.1 + image: orthancteam/orthanc-keycloak:labels-perm depends_on: [keycloak-db] restart: unless-stopped environment: From 72febcc91b5d8967b6aaf57327aaf0e5b5364fcb Mon Sep 17 00:00:00 2001 From: Alain Mazy Date: Tue, 12 Sep 2023 16:47:54 +0200 Subject: [PATCH 5/7] fix tests --- sources/orthanc_auth_service/shares/tokens_manager.py | 4 ++-- sources/orthanc_auth_service/shares/utils/utils.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/sources/orthanc_auth_service/shares/tokens_manager.py b/sources/orthanc_auth_service/shares/tokens_manager.py index f4d7d58..1470148 100644 --- a/sources/orthanc_auth_service/shares/tokens_manager.py +++ b/sources/orthanc_auth_service/shares/tokens_manager.py @@ -9,7 +9,7 @@ import logging import pytz import jwt -from dateutil import parser +from .utils.utils import DateTimeJSONEncoder logging.basicConfig(level=logging.INFO) @@ -98,7 +98,7 @@ def __init__(self, secret_key: str, server_id: Optional[str] = None): self.server_id_ = server_id def _encode_token(self, request: TokenCreationRequest) -> str: - return jwt.encode(request.dict(), self.secret_key_, algorithm="HS256") + return jwt.encode(request.model_dump(), self.secret_key_, algorithm="HS256", json_encoder=DateTimeJSONEncoder) def _decode_token(self, token: str) -> dict: try: diff --git a/sources/orthanc_auth_service/shares/utils/utils.py b/sources/orthanc_auth_service/shares/utils/utils.py index 4f9ff64..791450a 100644 --- a/sources/orthanc_auth_service/shares/utils/utils.py +++ b/sources/orthanc_auth_service/shares/utils/utils.py @@ -4,7 +4,8 @@ import os import logging - +import datetime +from json import JSONEncoder # try to read a secret first from a secret file or from an env var. # stop execution if not @@ -29,3 +30,9 @@ def is_secret_defined(name: str) -> bool: return os.environ.get(name) is not None + +class DateTimeJSONEncoder(JSONEncoder): + # Override the default method + def default(self, obj): + if isinstance(obj, (datetime.date, datetime.datetime)): + return obj.isoformat() \ No newline at end of file From 18869c9593615941cfebd6ded4f3a1b7d5ca4da2 Mon Sep 17 00:00:00 2001 From: Alain Mazy Date: Tue, 19 Sep 2023 12:55:31 +0200 Subject: [PATCH 6/7] updated demo for labels permissions --- minimal-setup/basic-auth/docker-compose.yml | 8 ++++---- minimal-setup/keycloak-meddream-full/README.md | 2 +- .../keycloak-meddream-full/docker-compose.yml | 17 ++++++++++------- .../keycloak-meddream-full/permissions.json | 16 ++++++++++++++++ minimal-setup/keycloak/README.md | 15 ++++++++++----- minimal-setup/keycloak/docker-compose.yml | 15 ++++++++++----- minimal-setup/keycloak/permissions.json | 16 ++++++++++++++++ release-notes.md | 5 ++++- sources/orthanc_auth_service/permissions.json | 8 ++++---- 9 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 minimal-setup/keycloak-meddream-full/permissions.json create mode 100644 minimal-setup/keycloak/permissions.json diff --git a/minimal-setup/basic-auth/docker-compose.yml b/minimal-setup/basic-auth/docker-compose.yml index 14804d4..56cbd0d 100644 --- a/minimal-setup/basic-auth/docker-compose.yml +++ b/minimal-setup/basic-auth/docker-compose.yml @@ -6,7 +6,7 @@ version: "3" services: nginx: - image: orthancteam/orthanc-nginx:23.6.1 + image: orthancteam/orthanc-nginx:23.9.0 depends_on: [orthanc, orthanc-auth-service, orthanc-for-shares] restart: unless-stopped ports: ["80:80"] @@ -22,7 +22,7 @@ services: ENABLE_HTTPS: "false" orthanc: - image: osimis/orthanc:current + image: osimis/orthanc:23.9.0 volumes: - orthanc-storage:/var/lib/orthanc/db depends_on: [orthanc-db] @@ -60,7 +60,7 @@ services: } orthanc-for-shares: - image: osimis/orthanc:current + image: osimis/orthanc:23.9.0 volumes: - orthanc-storage:/var/lib/orthanc/db depends_on: [orthanc-db] @@ -102,7 +102,7 @@ services: } orthanc-auth-service: - image: orthancteam/orthanc-auth-service:labels-perm + image: orthancteam/orthanc-auth-service:23.9.0 restart: unless-stopped environment: SECRET_KEY: "change-me-I-am-a-secret-key" diff --git a/minimal-setup/keycloak-meddream-full/README.md b/minimal-setup/keycloak-meddream-full/README.md index 0b3b703..6dab5b1 100644 --- a/minimal-setup/keycloak-meddream-full/README.md +++ b/minimal-setup/keycloak-meddream-full/README.md @@ -38,7 +38,7 @@ To start the setup, type: `docker compose up`. Some containers will restart mul ## As an admin user -- Open the Orthanc UI at [http://localhost/orthanc/ui/app/](http://localhost/orthanc/ui/app/) (login/pwd: `orthanc`/`change-me`) +- Open the Orthanc UI at [http://localhost/orthanc/ui/app/](http://localhost/orthanc/ui/app/) (login/pwd: `admin`/`change-me`) - upload a dicom file in Orthanc - you may click `view the study in MedDream` - On the uploaded file, click on the `Share` button and then on `Share` in the dialog box and then on `Copy and close` diff --git a/minimal-setup/keycloak-meddream-full/docker-compose.yml b/minimal-setup/keycloak-meddream-full/docker-compose.yml index 5d7575a..c18a6a8 100644 --- a/minimal-setup/keycloak-meddream-full/docker-compose.yml +++ b/minimal-setup/keycloak-meddream-full/docker-compose.yml @@ -6,7 +6,7 @@ version: "3" services: nginx: - image: orthancteam/orthanc-nginx:23.6.1 + image: orthancteam/orthanc-nginx:23.9.0 depends_on: [orthanc, orthanc-auth-service, orthanc-for-api, meddream-viewer, keycloak] restart: unless-stopped ports: ["80:80"] @@ -24,7 +24,7 @@ services: ENABLE_ORTHANC_FOR_API: "true" orthanc: - image: osimis/orthanc:23.6.1 + image: osimis/orthanc:23.9.0 volumes: - orthanc-storage:/var/lib/orthanc/db depends_on: [orthanc-db] @@ -75,8 +75,11 @@ services: } orthanc-auth-service: - image: orthancteam/orthanc-auth-service:23.6.1 + image: orthancteam/orthanc-auth-service:23.9.0 depends_on: [keycloak, meddream-token-service] + # permissions can be customized in the permissions.json file + volumes: + - ./permissions.json:/orthanc_auth_service/permissions.json restart: unless-stopped environment: SECRET_KEY: "change-me-I-am-a-secret-key" @@ -98,7 +101,7 @@ services: POSTGRES_HOST_AUTH_METHOD: "trust" keycloak: - image: orthancteam/orthanc-keycloak:23.6.1 + image: orthancteam/orthanc-keycloak:23.9.0 depends_on: [keycloak-db] restart: unless-stopped environment: @@ -121,11 +124,11 @@ services: POSTGRES_DB: "keycloak" meddream-token-service: - image: orthancteam/meddream-token-service:23.6.1 + image: orthancteam/meddream-token-service:23.9.0 restart: unless-stopped meddream-viewer: - image: orthancteam/meddream-viewer:23.6.1 + image: orthancteam/meddream-viewer:23.9.0 restart: unless-stopped depends_on: - orthanc-for-api @@ -142,7 +145,7 @@ services: # An orthanc dedicated for API accesses and also used by MedDream orthanc-for-api: - image: osimis/orthanc:23.6.1 + image: osimis/orthanc:23.9.0 volumes: - orthanc-storage:/var/lib/orthanc/db - ./meddream-plugin.py:/scripts/meddream-plugin.py diff --git a/minimal-setup/keycloak-meddream-full/permissions.json b/minimal-setup/keycloak-meddream-full/permissions.json new file mode 100644 index 0000000..db6351f --- /dev/null +++ b/minimal-setup/keycloak-meddream-full/permissions.json @@ -0,0 +1,16 @@ +{ + "roles" : { + "admin-role": { + "permissions": ["all"], + "authorized_labels": ["*"] + }, + "doctor-role": { + "permissions":["view", "download", "share", "send"], + "authorized_labels": ["*"] + }, + "external-role": { + "permissions":["view", "download"], + "authorized_labels": ["external"] + } + } +} \ No newline at end of file diff --git a/minimal-setup/keycloak/README.md b/minimal-setup/keycloak/README.md index 98c8ec0..8744157 100644 --- a/minimal-setup/keycloak/README.md +++ b/minimal-setup/keycloak/README.md @@ -35,9 +35,10 @@ To start the setup, type: `docker compose up`. Some containers will restart mul ## As an admin user -- Open the Orthanc UI at [http://localhost/orthanc/ui/app/](http://localhost/orthanc/ui/app/) (login/pwd: `orthanc`/`change-me`) -- upload a dicom file in Orthanc -- On the uploaded file, click on the `Share` button and then on `Share` in the dialog box and then on `Copy and close` +- Open the Orthanc UI at [http://localhost/orthanc/ui/app/](http://localhost/orthanc/ui/app/) (login/pwd: `admin`/`change-me`) +- upload a few dicom studies in Orthanc +- Add the `external` label to a few of the studies +- On one of the uploaded studies, click on the `Share` button and then on `Share` in the dialog box and then on `Copy and close` - Keep the link in your clipboard. You may share this link with an external user. - Go to `Profile` -> `Logout` @@ -52,11 +53,15 @@ To start the setup, type: `docker compose up`. Some containers will restart mul - Open the Orthanc UI at [http://localhost/orthanc/ui/app/](http://localhost/orthanc/ui/app/) (login/pwd: `doctor`/`change-me`) - The doctor user is a restricted user who can browse the whole set of studies but who can not upload/modify/delete them. +## As an external user + +- Open the Orthanc UI at [http://localhost/orthanc/ui/app/](http://localhost/orthanc/ui/app/) (login/pwd: `external`/`change-me`) +- This user can only see the studies that have been tagged with the `external` tag. # Accessing this demo from a remote client -If you wish to access this demo from a remote computer, you must tell the setup on which domain it is accessible (in this sample: `mydomain.com`). -Then, you should update these settings: +If you wish to access this demo from a remote computer, you must configure the domain on which this setup is accessible (in this sample: `mydomain.com`). +Update these settings: - orthanc: ORTHANC_JSON -> OrthancExplorer2 -> Keycloak -> "Url": "http://mydomain.com/keycloak/" - keycloak: KC_HOSTNAME_URL: "http://mydomain.com/keycloak" - keycloak: KC_HOSTNAME_ADMIN_URL: "http://mydomain.com/keycloak" \ No newline at end of file diff --git a/minimal-setup/keycloak/docker-compose.yml b/minimal-setup/keycloak/docker-compose.yml index f9b6cc7..f14af60 100644 --- a/minimal-setup/keycloak/docker-compose.yml +++ b/minimal-setup/keycloak/docker-compose.yml @@ -6,7 +6,7 @@ version: "3" services: nginx: - image: orthancteam/orthanc-nginx:23.6.1 + image: orthancteam/orthanc-nginx:23.9.0 depends_on: [orthanc, orthanc-auth-service, keycloak] restart: unless-stopped ports: ["80:80"] @@ -23,7 +23,7 @@ services: ENABLE_OHIF: "true" orthanc: - image: osimis/orthanc:current + image: osimis/orthanc:23.9.0 volumes: - orthanc-storage:/var/lib/orthanc/db depends_on: [orthanc-db] @@ -76,7 +76,10 @@ services: } orthanc-auth-service: - image: orthancteam/orthanc-auth-service:current + image: orthancteam/orthanc-auth-service:23.9.0 + # permissions can be customized in the permissions.json file + volumes: + - ./permissions.json:/orthanc_auth_service/permissions.json depends_on: [keycloak] restart: unless-stopped environment: @@ -98,7 +101,7 @@ services: POSTGRES_HOST_AUTH_METHOD: "trust" ohif: - image: orthancteam/ohif-v3:23.6.1 + image: orthancteam/ohif-v3:23.9.0 # uncomment if you want to customize ohif configuration # volumes: # - ./ohif-app-config.js:/usr/share/nginx/html/app-config.js @@ -106,9 +109,11 @@ services: keycloak: - image: orthancteam/orthanc-keycloak:labels-perm + image: orthancteam/orthanc-keycloak:23.9.0 depends_on: [keycloak-db] restart: unless-stopped +# healthcheck: +# test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] environment: KEYCLOAK_ADMIN: "admin" KEYCLOAK_ADMIN_PASSWORD: "change-me" diff --git a/minimal-setup/keycloak/permissions.json b/minimal-setup/keycloak/permissions.json new file mode 100644 index 0000000..db6351f --- /dev/null +++ b/minimal-setup/keycloak/permissions.json @@ -0,0 +1,16 @@ +{ + "roles" : { + "admin-role": { + "permissions": ["all"], + "authorized_labels": ["*"] + }, + "doctor-role": { + "permissions":["view", "download", "share", "send"], + "authorized_labels": ["*"] + }, + "external-role": { + "permissions":["view", "download"], + "authorized_labels": ["external"] + } + } +} \ No newline at end of file diff --git a/release-notes.md b/release-notes.md index 25cea6d..c692d84 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,8 +4,11 @@ SPDX-FileCopyrightText: 2022 - 2023 Orthanc Team SRL SPDX-License-Identifier: GPL-3.0-or-later --> -v x.x.x +v 23.9.0 ======== + +- added support of labels permissions (via `authorized_labels` in user roles and user profiles) + BREAKING CHANGES: - the format of the permissions.json file has changed to include `permissions` and `authorized_labels`. diff --git a/sources/orthanc_auth_service/permissions.json b/sources/orthanc_auth_service/permissions.json index 4acd4e5..db6351f 100644 --- a/sources/orthanc_auth_service/permissions.json +++ b/sources/orthanc_auth_service/permissions.json @@ -1,16 +1,16 @@ { "roles" : { - "admin": { + "admin-role": { "permissions": ["all"], "authorized_labels": ["*"] }, - "doctor": { + "doctor-role": { "permissions":["view", "download", "share", "send"], "authorized_labels": ["*"] }, - "students": { + "external-role": { "permissions":["view", "download"], - "authorized_labels": ["students"] + "authorized_labels": ["external"] } } } \ No newline at end of file From 7c76ef75425f23dc19ae7e2a531797704d6175e3 Mon Sep 17 00:00:00 2001 From: Alain Mazy Date: Tue, 19 Sep 2023 14:31:40 +0200 Subject: [PATCH 7/7] fix reuse license --- minimal-setup/keycloak-meddream-full/docker-compose.yml | 2 +- .../{permissions.json => permissions.jsonc} | 2 ++ minimal-setup/keycloak/docker-compose.yml | 2 +- minimal-setup/keycloak/{permissions.json => permissions.jsonc} | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) rename minimal-setup/keycloak-meddream-full/{permissions.json => permissions.jsonc} (72%) rename minimal-setup/keycloak/{permissions.json => permissions.jsonc} (72%) diff --git a/minimal-setup/keycloak-meddream-full/docker-compose.yml b/minimal-setup/keycloak-meddream-full/docker-compose.yml index c18a6a8..52f7c51 100644 --- a/minimal-setup/keycloak-meddream-full/docker-compose.yml +++ b/minimal-setup/keycloak-meddream-full/docker-compose.yml @@ -79,7 +79,7 @@ services: depends_on: [keycloak, meddream-token-service] # permissions can be customized in the permissions.json file volumes: - - ./permissions.json:/orthanc_auth_service/permissions.json + - ./permissions.jsonc:/orthanc_auth_service/permissions.json restart: unless-stopped environment: SECRET_KEY: "change-me-I-am-a-secret-key" diff --git a/minimal-setup/keycloak-meddream-full/permissions.json b/minimal-setup/keycloak-meddream-full/permissions.jsonc similarity index 72% rename from minimal-setup/keycloak-meddream-full/permissions.json rename to minimal-setup/keycloak-meddream-full/permissions.jsonc index db6351f..8624a25 100644 --- a/minimal-setup/keycloak-meddream-full/permissions.json +++ b/minimal-setup/keycloak-meddream-full/permissions.jsonc @@ -1,3 +1,5 @@ +// "SPDX-FileCopyrightText: 2022 - 2023 Orthanc Team SRL " +// SPDX-License-Identifier: CC0-1.0 { "roles" : { "admin-role": { diff --git a/minimal-setup/keycloak/docker-compose.yml b/minimal-setup/keycloak/docker-compose.yml index f14af60..62be7fb 100644 --- a/minimal-setup/keycloak/docker-compose.yml +++ b/minimal-setup/keycloak/docker-compose.yml @@ -79,7 +79,7 @@ services: image: orthancteam/orthanc-auth-service:23.9.0 # permissions can be customized in the permissions.json file volumes: - - ./permissions.json:/orthanc_auth_service/permissions.json + - ./permissions.jsonc:/orthanc_auth_service/permissions.json depends_on: [keycloak] restart: unless-stopped environment: diff --git a/minimal-setup/keycloak/permissions.json b/minimal-setup/keycloak/permissions.jsonc similarity index 72% rename from minimal-setup/keycloak/permissions.json rename to minimal-setup/keycloak/permissions.jsonc index db6351f..8624a25 100644 --- a/minimal-setup/keycloak/permissions.json +++ b/minimal-setup/keycloak/permissions.jsonc @@ -1,3 +1,5 @@ +// "SPDX-FileCopyrightText: 2022 - 2023 Orthanc Team SRL " +// SPDX-License-Identifier: CC0-1.0 { "roles" : { "admin-role": {