diff --git a/.dockerignore b/.dockerignore index 750935f33..67f1b2e98 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,16 @@ +# Output dist/ + +# Dependencies node_modules/ -.git + +# Root +README.md Dockerfile + +# Misc. Config +.git .DS_Store .gitignore -README.md .vscode .circleci -example/ diff --git a/docker/dcm4che/docker-compose-dcm4che.env b/docker/Nginx-Dcm4che/docker-compose-dcm4che.env similarity index 100% rename from docker/dcm4che/docker-compose-dcm4che.env rename to docker/Nginx-Dcm4che/docker-compose-dcm4che.env diff --git a/docker/docker-compose-dcm4che.yml b/docker/Nginx-Dcm4che/docker-compose-dcm4che.yml similarity index 100% rename from docker/docker-compose-dcm4che.yml rename to docker/Nginx-Dcm4che/docker-compose-dcm4che.yml diff --git a/docker/dcm4che/etc/localtime b/docker/Nginx-Dcm4che/etc/localtime similarity index 100% rename from docker/dcm4che/etc/localtime rename to docker/Nginx-Dcm4che/etc/localtime diff --git a/docker/dcm4che/etc/timezone b/docker/Nginx-Dcm4che/etc/timezone similarity index 100% rename from docker/dcm4che/etc/timezone rename to docker/Nginx-Dcm4che/etc/timezone diff --git a/docker/nginx-proxy/conf/nginx.conf b/docker/Nginx-Dcm4che/nginx-proxy/conf/nginx.conf similarity index 100% rename from docker/nginx-proxy/conf/nginx.conf rename to docker/Nginx-Dcm4che/nginx-proxy/conf/nginx.conf diff --git a/docker/Nginx-Orthanc/config/nginx.conf b/docker/Nginx-Orthanc/config/nginx.conf new file mode 100644 index 000000000..c38ee5813 --- /dev/null +++ b/docker/Nginx-Orthanc/config/nginx.conf @@ -0,0 +1,48 @@ +worker_processes 1; + +events { worker_connections 1024; } + +http { + + upstream orthanc-server { + server orthanc:8042; + } + + server { + listen [::]:80 default_server; + listen 80; + + # CORS Magic + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow_Credentials' 'true'; + add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH'; + + location / { + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow_Credentials' 'true'; + add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + proxy_pass http://orthanc:8042; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + # CORS Magic + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow_Credentials' 'true'; + add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH'; + } + } +} diff --git a/docker/Nginx-Orthanc/config/orthanc.json b/docker/Nginx-Orthanc/config/orthanc.json new file mode 100644 index 000000000..2e10723c0 --- /dev/null +++ b/docker/Nginx-Orthanc/config/orthanc.json @@ -0,0 +1,89 @@ +{ + "Name": "Orthanc inside Docker", + "StorageDirectory": "/var/lib/orthanc/db", + "IndexDirectory": "/var/lib/orthanc/db", + "StorageCompression": false, + "MaximumStorageSize": 0, + "MaximumPatientCount": 0, + "LuaScripts": [], + "Plugins": ["/usr/share/orthanc/plugins", "/usr/local/share/orthanc/plugins"], + "ConcurrentJobs": 2, + "HttpServerEnabled": true, + "HttpPort": 8042, + "HttpDescribeErrors": true, + "HttpCompressionEnabled": true, + "DicomServerEnabled": true, + "DicomAet": "ORTHANC", + "DicomCheckCalledAet": false, + "DicomPort": 4242, + "DefaultEncoding": "Latin1", + "DeflatedTransferSyntaxAccepted": true, + "JpegTransferSyntaxAccepted": true, + "Jpeg2000TransferSyntaxAccepted": true, + "JpegLosslessTransferSyntaxAccepted": true, + "JpipTransferSyntaxAccepted": true, + "Mpeg2TransferSyntaxAccepted": true, + "RleTransferSyntaxAccepted": true, + "UnknownSopClassAccepted": false, + "DicomScpTimeout": 30, + + "RemoteAccessAllowed": true, + "SslEnabled": false, + "SslCertificate": "certificate.pem", + "AuthenticationEnabled": false, + "RegisteredUsers": { + "test": "test" + }, + "DicomModalities": {}, + "DicomModalitiesInDatabase": false, + "DicomAlwaysAllowEcho": true, + "DicomAlwaysAllowStore": true, + "DicomCheckModalityHost": false, + "DicomScuTimeout": 10, + "OrthancPeers": {}, + "OrthancPeersInDatabase": false, + "HttpProxy": "", + + "HttpVerbose": true, + + "HttpTimeout": 10, + "HttpsVerifyPeers": true, + "HttpsCACertificates": "", + "UserMetadata": {}, + "UserContentType": {}, + "StableAge": 60, + "StrictAetComparison": false, + "StoreMD5ForAttachments": true, + "LimitFindResults": 0, + "LimitFindInstances": 0, + "LimitJobs": 10, + "LogExportedResources": false, + "KeepAlive": true, + "TcpNoDelay": true, + "HttpThreadsCount": 50, + "StoreDicom": true, + "DicomAssociationCloseDelay": 5, + "QueryRetrieveSize": 10, + "CaseSensitivePN": false, + "LoadPrivateDictionary": true, + "Dictionary": {}, + "SynchronousCMove": true, + "JobsHistorySize": 10, + "SaveJobs": true, + "OverwriteInstances": false, + "MediaArchiveSize": 1, + "StorageAccessOnFind": "Always", + "MetricsEnabled": true, + + "DicomWeb": { + "Enable": true, + "Root": "/dicom-web/", + "EnableWado": true, + "WadoRoot": "/wado", + "Host": "127.0.0.1", + "Ssl": false, + "StowMaxInstances": 10, + "StowMaxSize": 10, + "QidoCaseSensitive": false + } +} diff --git a/docker/Nginx-Orthanc/docker-compose.yml b/docker/Nginx-Orthanc/docker-compose.yml new file mode 100644 index 000000000..26e797514 --- /dev/null +++ b/docker/Nginx-Orthanc/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.5' + +services: + proxy: + image: nginx:1.15-alpine + ports: + - 8899:80 + volumes: + - ./config/nginx.conf:/etc/nginx/nginx.conf:ro + restart: unless-stopped + + # LINK: https://hub.docker.com/r/jodogne/orthanc-plugins/ + # TODO: Update to use Postgres + # https://github.com/mrts/docker-postgresql-multiple-databases + orthanc: + image: jodogne/orthanc-plugins:1.5.6 + hostname: orthanc + volumes: + # Config + - ./config/orthanc.json:/etc/orthanc/orthanc.json:ro + # Persist data + - ./volumes/orthanc-db/:/var/lib/orthanc/db/ + # ports: + # - '4242:4242' # DICOM + # - '8042:8042' # Web + restart: unless-stopped diff --git a/docker/Nginx-Orthanc/volumes/orthanc-db/.githold b/docker/Nginx-Orthanc/volumes/orthanc-db/.githold new file mode 100644 index 000000000..e69de29bb diff --git a/docker/Nginx-Orthanc/volumes/orthanc-db/.gitignore b/docker/Nginx-Orthanc/volumes/orthanc-db/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/docker/Nginx-Orthanc/volumes/orthanc-db/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docker/docker-compose-publicserver.yml b/docker/Nginx/docker-compose-publicserver.yml similarity index 100% rename from docker/docker-compose-publicserver.yml rename to docker/Nginx/docker-compose-publicserver.yml diff --git a/docker/OpenResty-Orthanc-Keycloak/.dockerignore b/docker/OpenResty-Orthanc-Keycloak/.dockerignore new file mode 100644 index 000000000..67f1b2e98 --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/.dockerignore @@ -0,0 +1,16 @@ +# Output +dist/ + +# Dependencies +node_modules/ + +# Root +README.md +Dockerfile + +# Misc. Config +.git +.DS_Store +.gitignore +.vscode +.circleci diff --git a/docker/OpenResty-Orthanc-Keycloak/.env b/docker/OpenResty-Orthanc-Keycloak/.env new file mode 100644 index 000000000..6989ff3fc --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/.env @@ -0,0 +1,5 @@ +# Docker ENV and ARG Variables +# ---------------------------- +# https://vsupalov.com/docker-arg-env-variable-guide/ +# +# diff --git a/docker/OpenResty-Orthanc-Keycloak/README.md b/docker/OpenResty-Orthanc-Keycloak/README.md new file mode 100644 index 000000000..3582f8dbf --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/README.md @@ -0,0 +1,31 @@ +# Guide + +Build docker container using: + +- Build: `docker build -t authproxy .` +- Tag: `docker tag authproxy:latest authproxy:staging` + +> Start your docker container, and volume mount the directory containing the +> `nginx-keycloak.conf` configuration file. We also mount the current directory +> under `/usr/share/nginx/html` so any html files in the current directory will +> be hosted behind the authenticating proxy. Finally, we mapped port 80 on the +> host to port 80 in the container. + +`docker run -d -it -p 80:80 -v $PWD/:/config -v /:/usr/share/nginx/html authproxy -c /config/nginx.conf` +docker run -d -it -p 80:80 -v /:/config -v /:/usr/share/nginx/html authproxy -c +/config/nginx-keycloak.conf + +## Orthanc + +- Configuration + - http://book.orthanc-server.com/users/configuration.html#configuration + +## Keycloak + +### Setup + +Library we use to manage OpenID-Connect `implicit` flow: +https://github.com/maxmantz/redux-oidc + +- Set admin user/pass in `docker-compose` +- What are realms? diff --git a/docker/OpenResty-Orthanc-Keycloak/config/nginx.conf b/docker/OpenResty-Orthanc-Keycloak/config/nginx.conf new file mode 100644 index 000000000..7360c3361 --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/config/nginx.conf @@ -0,0 +1,259 @@ +worker_processes 2; +error_log /var/logs/nginx/mydomain.error.log; +pid /var/run/nginx.pid; +include /usr/share/nginx/modules/*.conf; # See /usr/share/doc/nginx/README.dynamic. + +events { + worker_connections 1024; ## Default: 1024 + use epoll; # http://nginx.org/en/docs/events.html + multi_accept on; # http://nginx.org/en/docs/ngx_core_module.html#multi_accept +} + +# Core Modules Docs: +# http://nginx.org/en/docs/http/ngx_http_core_module.html +http { + include '/usr/local/openresty/nginx/conf/mime.types'; + default_type application/octet-stream; + + keepalive_timeout 65; + keepalive_requests 100000; + tcp_nopush on; + tcp_nodelay on; + + # lua_ settings + # + lua_package_path '/usr/local/openresty/lualib/?.lua;;'; + lua_shared_dict discovery 1m; # cache for discovery metadata documents + lua_shared_dict jwks 1m; # cache for JWKs + # lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + + variables_hash_max_size 2048; + server_names_hash_bucket_size 128; + server_tokens off; + + resolver 8.8.8.8 valid=30s ipv6=off; + resolver_timeout 11s; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + # No idea what this is doing + # https://stackoverflow.com/a/5877989/1867984 + # upstream upstream_server { + # # server 10.100.4.200:1010 max_fails=3 fail_timeout=30s; + # server 127.0.0.1: + # } + + # Nginx `listener` block + server { + listen [::]:80 default_server; + listen 80; + # listen 443 ssl; + access_log /var/logs/nginx/mydomain.access.log; + + # Domain to protect + server_name 127.0.0.1 localhost; # mydomain.com; + proxy_intercept_errors off; + # ssl_certificate /etc/letsencrypt/live/mydomain.co.uk/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/mydomain.co.uk/privkey.pem; + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_comp_level 9; + etag on; + + # https://github.com/bungle/lua-resty-session/issues/15 + set $session_check_ssi off; + lua_code_cache off; + set $session_secret Eeko7aeb6iu5Wohch9Loo1aitha0ahd1; + set $session_storage cookie; + + server_tokens off; # Hides server version num + + # [PROTECTED] Reverse Proxy for `orthanc` admin + # + location /pacs-admin/ { + access_by_lua_block { + local opts = { + redirect_uri = "http://127.0.0.1/pacs-admin/admin", + discovery = "http://127.0.0.1/auth/realms/ohif/.well-known/openid-configuration", + token_endpoint_auth_method = "client_secret_basic", + client_id = "pacs", + client_secret = "66279641-eba6-47f5-9fdb-70c4ac74d548", + client_jwt_assertion_expires_in = 60 * 60, + ssl_verify = "no", + scope = "openid email profile", + refresh_session_interval = 900, + redirect_uri_scheme = "http", + redirect_after_logout_uri = "/", + session_contents = {id_token=true} + } + + -- call authenticate for OpenID Connect user authentication + local res, err = require("resty.openidc").authenticate(opts) + + if err or not res then + ngx.print(err) + ngx.status = 200 + ngx.say(err and err or "no access_token provided") + ngx.exit(ngx.HTTP_FORBIDDEN) + end + + -- Or set cookie? + -- ngx.req.set_header("Authorization", "Bearer " .. res.access_token) + ngx.req.set_header("X-USER", res.id_token.sub) + } + + + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + expires 0; + add_header Cache-Control private; + + proxy_pass http://orthanc:8042/; + } + + # [PROTECTED] Reverse Proxy for `orthanc` APIs (including DICOMWeb) + # + location /pacs/ { + access_by_lua_block { + local opts = { + discovery = "http://127.0.0.1/auth/realms/ohif/.well-known/openid-configuration", + } + + -- call bearer_jwt_verify for OAuth 2.0 JWT validation + local res, err = require("resty.openidc").bearer_jwt_verify(opts) + + if err or not res then + ngx.status = 403 + ngx.say(err and err or "no access_token provided") + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } + + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection "upgrade"; + + expires 0; + add_header Cache-Control private; + + proxy_pass http://orthanc:8042/; + + # By default, this endpoint is protected by CORS (cross-origin-resource-sharing) + # You can add headers to allow other domains to request this resource. + # See the "Updating CORS Settings" example below + } + + # Keycloak + # + location /auth/ { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + + proxy_pass http://keycloak:8080/auth/; + } + + # Do not cache sw.js, required for offline-first updates. + location /sw.js { + add_header Cache-Control "no-cache"; + proxy_cache_bypass $http_pragma; + proxy_cache_revalidate on; + expires off; + access_log off; + } + + # Single Page App + # Try files, fallback to index.html + # + location / { + alias /var/www/html/; + index index.html; + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + # EXAMPLE: Reverse Proxy, no auth + # [UNPROTECTED] reverse proxy for `orthanc` + # + # location /pacs/ { + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $remote_addr; + # proxy_set_header Host $host; + # + # proxy_pass http://orthanc:8042/; + # + # # OR + # # rewrite ^/pacs(.*) /$1 break; + # # proxy_pass http://orthanc:8042; + # } + + # EXAMPLE: Modifying headers to allow requests from other domains + # IE. Updating CORS settings + # + # location / { + # if ($request_method = 'OPTIONS') { + # add_header 'Access-Control-Allow-Origin' '*'; + # add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + # # + # # Custom headers and headers various browsers *should* be OK with but aren't + # # + # add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; + # # + # # Tell client that this pre-flight info is valid for 20 days + # # + # add_header 'Access-Control-Allow-Headers' 'Authorization'; + # add_header 'Access-Control-Allow-Credentials' true; + # add_header 'Access-Control-Max-Age' 1728000; + # add_header 'Content-Length' 0; + # return 204; + # } + # if ($request_method = 'POST') { + # add_header 'Access-Control-Allow-Origin' '*'; + # add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + # add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; + # add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; + # } + # if ($request_method = 'GET') { + # add_header 'Access-Control-Allow-Origin' '*'; + # add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + # add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; + # add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; + # add_header 'Access-Control-Allow-Headers' 'Authorization'; + # add_header 'Access-Control-Allow-Credentials' true; + # } + # + # # proxy_http_version 1.1; + # + # # proxy_set_header Host $host; + # # proxy_set_header X-Real-IP $remote_addr; + # # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # # proxy_set_header X-Forwarded-Proto $scheme; + # + # proxy_pass http://orthanc:8042; + # } + + # EXAMPLE: Redirect server error pages to the static page /40x.html + # + # error_page 404 /404.html; + # location = /40x.html { + # } + + # EXAMPLE: Redirect server error pages to the static page /50x.html + # + # error_page 500 502 503 504 /50x.html; + # location = /50x.html { + # } + } +} diff --git a/docker/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json b/docker/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json new file mode 100644 index 000000000..3f0e6118a --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json @@ -0,0 +1,1876 @@ +{ + "id": "ohif", + "realm": "ohif", + "displayName": "OHIF", + "displayNameHtml": "
OHIF
", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "6c3af6bd-09e5-41ab-a997-ecb926e06a9c", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "ohif", + "attributes": {} + }, + { + "id": "eb61ea0f-059d-4aeb-8179-6d223882be4e", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "ohif", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "20b8764b-3db0-47bf-80f3-fdc5e2a80651", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "b85a57ed-b33a-41b2-8748-d02f9f334b42", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "view-events", + "query-realms", + "query-groups", + "manage-clients", + "query-clients", + "impersonation", + "manage-identity-providers", + "view-identity-providers", + "query-users", + "manage-realm", + "manage-authorization", + "view-authorization", + "view-realm", + "manage-users", + "view-clients", + "manage-events", + "create-client", + "view-users" + ] + } + }, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "5fdc7311-003e-4644-b497-3b88f0fd2771", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "cb1ebb2f-7772-417e-8ed6-9f144e53aa66", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "0c2b9722-d7cb-49c1-9eba-216b8485f0a9", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "e60cd69e-6ebb-4e2b-aefc-7529f62bd410", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "003085ed-b327-4dc2-bcfe-b97f25dbfdb2", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "97678c0b-a333-443b-af67-39be0c9b9656", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "f96be28a-ed19-4e0c-a2cc-85ba3daed401", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "081be3e8-f3a9-4d2a-94ee-ec357082d019", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "376fec14-86c7-4986-8f09-9d4b93880c57", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "1c105710-6a0e-40bb-8e45-924017eff426", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "6f0f4bad-7761-487d-babd-62500ca02380", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "089145da-eb3c-4a06-a623-01f1c0278710", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "59cf0f7d-0ccc-4b2d-ab23-dd00a95727d2", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "bbf8231a-f92c-4c7d-a962-f1ffc3a77486", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": ["query-clients"] + } + }, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "b76f0563-eb1a-4e85-853d-2c5fd41820ab", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "bc00ccc2-f675-4649-a1fc-0138da85c2ef", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + }, + { + "id": "982241da-f30d-4a9b-a425-d69fb5f1b0ee", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": ["query-groups", "query-users"] + } + }, + "clientRole": true, + "containerId": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "attributes": {} + } + ], + "pacs": [ + { + "id": "46283d0b-9b8b-46f2-a111-dfa460103f2f", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "3785434d-2af8-478c-b135-f0b11d1d3205", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "broker": [ + { + "id": "49b1694a-21f5-4eb0-a9c4-4f847313d722", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "f7f76add-411b-420d-9be1-bd120ed99918", + "attributes": {} + } + ], + "ohif-viewer": [], + "account": [ + { + "id": "6ae38e13-39e5-4000-962f-ef0175f57c60", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "e6bee9db-9634-4cd2-93d3-cdaa1f011dd8", + "attributes": {} + }, + { + "id": "368522c4-b8f0-40af-ac53-b96496c4a44a", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": ["manage-account-links"] + } + }, + "clientRole": true, + "containerId": "e6bee9db-9634-4cd2-93d3-cdaa1f011dd8", + "attributes": {} + }, + { + "id": "af2a7fbc-38da-49cd-8495-c798530fe251", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "e6bee9db-9634-4cd2-93d3-cdaa1f011dd8", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRoles": ["offline_access", "uma_authorization"], + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": ["FreeOTP", "Google Authenticator"], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": ["offline_access"] + } + ], + "clients": [ + { + "id": "315fbb4d-f9c8-46f6-888e-d6c69d415ee0", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "84e7b84d-ab74-452e-8a24-a0e657f100ea", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f7dd8587-4035-4590-a1c0-ebf576c4dfe3", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "baseUrl": "/auth/admin/ohif/console/index.html", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": ["/auth/admin/ohif/console/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "3c48b046-72a2-4779-abb5-760c58fe2c19", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f7f76add-411b-420d-9be1-bd120ed99918", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "3785434d-2af8-478c-b135-f0b11d1d3205", + "clientId": "pacs", + "rootUrl": "http://127.0.0.1", + "baseUrl": "/pacs-admin", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "66279641-eba6-47f5-9fdb-70c4ac74d548", + "redirectUris": ["*"], + "webOrigins": ["*"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "6aad2e40-2917-4549-a823-b8765ea4b13f", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "560fea50-208a-487a-82ff-de6edd81eb80", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "248bf04e-057a-46f4-be6c-1a64728e0203", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:pacs:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "0ba897de-b178-44e6-8dd6-dec09b066808", + "uris": ["/*"] + } + ], + "policies": [ + { + "id": "4b79719f-ed30-451b-8ad1-7ba516a2b3f2", + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "id": "4071a9c3-5eec-417f-898e-cb0e7fe11f85", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:pacs:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [] + } + }, + { + "id": "e6bee9db-9634-4cd2-93d3-cdaa1f011dd8", + "clientId": "account", + "name": "${client_account}", + "baseUrl": "/auth/realms/ohif/account", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "defaultRoles": ["view-profile", "manage-account"], + "redirectUris": ["/auth/realms/ohif/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "53d51818-e1bf-4fc2-aa20-5541f2646f12", + "clientId": "ohif-viewer", + "rootUrl": "http://127.0.0.1", + "adminUrl": "http://127.0.0.1", + "baseUrl": "/", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": ["*"], + "webOrigins": ["http://127.0.0.1"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "login_theme": "ohif", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "role_list", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "6ae5b114-291d-4265-826a-59d66c7257c4", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "53f31712-86fe-429a-897f-56f6a68ff04a", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "400bbe92-0c89-42a5-9deb-846465f37832", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "bf928a3a-12bd-4fcc-9c8c-a7302161af9c", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "47ff8af3-462d-450d-b72e-ed8c280f2df7", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "3a2f720d-06aa-4a21-bb8b-a2fe8ff740ba", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "f312bcba-a892-4fa1-ab66-a784cb3359f7", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + }, + { + "id": "a9ab2bd5-835d-4f5f-aa07-2845ccd3d361", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "cf2ec6d5-ac10-4d67-9c10-1cf6f83d1ba5", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "09747e03-0974-4937-a444-2170c1185842", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "6e91d83d-746b-499a-9f1c-76d821f2c51d", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "b185e72e-0fef-43ed-971b-a8e4c420c8b9", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "775a0614-070f-4ee3-9eb5-7dace1a3ee9f", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "5e9c8afa-e924-4378-b957-391c204dfa97", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "7361627f-bb6e-4ddf-ba05-9f2e449fd636", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "99f6051f-a4dc-46fc-bc46-8fdb224028cb", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "4401b7fa-7a4a-416e-91dd-bd94835bcd05", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ee4aefa7-398a-4ced-b9fb-f25ad83a1e4e", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "fddd6ec8-99d1-4654-af68-5a08285845a7", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "4d70b18c-6ac1-4210-be64-3d975d127eeb", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "e60ad476-a109-4f6d-8b8f-638ea737d641", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "1ecac05a-a740-4f49-bdde-66df4b57fdab", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "7adb553b-e335-412d-8633-21b1e68fbf5c", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "b51200b1-ffb2-4222-989d-c78af76318d4", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "cd73c78e-5e78-422d-8f68-818ef766529d", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "7242207c-dd96-4932-8914-2f36e2d91689", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "237dace9-0b8e-4e4c-82c7-b3d588630009", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "88c56ac7-47a8-45e2-8774-1757b0df8d87", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "ec68ab29-95cd-476c-a528-fa8d0ca97ae9", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "0245d1ab-f14b-4bb2-97d2-6f0e1bab41fe", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "0d95b1c0-85d9-4dfa-b269-e07a890b023d", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "7c391683-f6a1-4cd7-aca9-2f6690f1700d", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "532b1821-92bf-4ff7-95b6-cba1f2ca3545", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e606a1b0-6772-441e-9799-44ec896ca13d", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "28c9327f-b38d-4dd8-81f7-3a5bc95c019f", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": ["jboss-logging"], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "abc1fcf9-610c-4682-a68c-f2786d19ca2f", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "ce5e7488-00e2-4ce5-99b4-b77ea86a1470", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "73fcfae3-4890-4c85-becf-f43b29b24db9", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": ["200"] + } + }, + { + "id": "5faf17f3-6b8f-40ed-9bdc-ae74a638a197", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "fe7ccadb-4534-4944-a245-bd78633144af", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "483b67ab-fe56-4ba9-ab65-3b9ac59bf048", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": ["true"], + "client-uris-must-match": ["true"] + } + }, + { + "id": "ebf46274-4329-40c7-8342-7a9c2d3181e5", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "4a34d375-8480-4368-8eb7-516e382e5f28", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "527b473f-81bf-4bc1-8c41-667bcefa7414", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": ["100"] + } + }, + { + "id": "ad0ab7c0-d4e0-470c-a43c-f4c4d71d5f9e", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": ["100"] + } + }, + { + "id": "6f1171b4-22eb-4a94-ae64-7c33ed2b8817", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": ["100"], + "algorithm": ["HS256"] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "0131114f-fc73-4b02-aa5e-bbf7502229e2", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "0c5dd2e0-f444-4406-a8f7-88248808bc2e", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "d212c8b1-e4dc-4e19-8ed1-d564406885af", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "1d9bc1ff-3b71-4730-ac2c-fae8e1babcdd", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9a4f37bf-d5d8-41e9-ac3e-ed41575b9ec9", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "OPTIONAL", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "898f145d-bb61-49d2-9304-43f42a482199", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9445cbe0-4307-490e-b996-026391c5064b", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "fcb61660-2e54-4434-be96-ce0b5e5bae13", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "5af7f92b-06d6-4aa4-861d-edbeb917ad8f", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "3829b8aa-55c8-401a-8684-88c36b74502a", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "618d1456-42bb-4737-aa06-f5bde16480fc", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "bdd6828c-026b-4a31-9b20-88c1816c7f08", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "requirement": "OPTIONAL", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "98ff3c64-bf5e-4101-b218-6dc3b7717f2c", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "f0446a79-4b15-4c18-abe7-a316f984ece4", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "3a38d4a4-dc5b-4020-9a94-64b61ad9c997", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "_browser_header.xXSSProtection": "1; mode=block", + "_browser_header.xFrameOptions": "SAMEORIGIN", + "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains", + "permanentLockout": "false", + "quickLoginCheckMilliSeconds": "1000", + "displayName": "OHIF", + "_browser_header.xRobotsTag": "none", + "maxFailureWaitSeconds": "900", + "minimumQuickLoginWaitSeconds": "60", + "displayNameHtml": "
OHIF
", + "failureFactor": "30", + "actionTokenGeneratedByUserLifespan": "300", + "maxDeltaTimeSeconds": "43200", + "_browser_header.xContentTypeOptions": "nosniff", + "offlineSessionMaxLifespan": "5184000", + "actionTokenGeneratedByAdminLifespan": "43200", + "_browser_header.contentSecurityPolicyReportOnly": "", + "bruteForceProtected": "false", + "_browser_header.contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "waitIncrementSeconds": "60", + "offlineSessionMaxLifespanEnabled": "false" + }, + "keycloakVersion": "6.0.1", + "userManagedAccessAllowed": false +} diff --git a/docker/OpenResty-Orthanc-Keycloak/config/orthanc.json b/docker/OpenResty-Orthanc-Keycloak/config/orthanc.json new file mode 100644 index 000000000..2e10723c0 --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/config/orthanc.json @@ -0,0 +1,89 @@ +{ + "Name": "Orthanc inside Docker", + "StorageDirectory": "/var/lib/orthanc/db", + "IndexDirectory": "/var/lib/orthanc/db", + "StorageCompression": false, + "MaximumStorageSize": 0, + "MaximumPatientCount": 0, + "LuaScripts": [], + "Plugins": ["/usr/share/orthanc/plugins", "/usr/local/share/orthanc/plugins"], + "ConcurrentJobs": 2, + "HttpServerEnabled": true, + "HttpPort": 8042, + "HttpDescribeErrors": true, + "HttpCompressionEnabled": true, + "DicomServerEnabled": true, + "DicomAet": "ORTHANC", + "DicomCheckCalledAet": false, + "DicomPort": 4242, + "DefaultEncoding": "Latin1", + "DeflatedTransferSyntaxAccepted": true, + "JpegTransferSyntaxAccepted": true, + "Jpeg2000TransferSyntaxAccepted": true, + "JpegLosslessTransferSyntaxAccepted": true, + "JpipTransferSyntaxAccepted": true, + "Mpeg2TransferSyntaxAccepted": true, + "RleTransferSyntaxAccepted": true, + "UnknownSopClassAccepted": false, + "DicomScpTimeout": 30, + + "RemoteAccessAllowed": true, + "SslEnabled": false, + "SslCertificate": "certificate.pem", + "AuthenticationEnabled": false, + "RegisteredUsers": { + "test": "test" + }, + "DicomModalities": {}, + "DicomModalitiesInDatabase": false, + "DicomAlwaysAllowEcho": true, + "DicomAlwaysAllowStore": true, + "DicomCheckModalityHost": false, + "DicomScuTimeout": 10, + "OrthancPeers": {}, + "OrthancPeersInDatabase": false, + "HttpProxy": "", + + "HttpVerbose": true, + + "HttpTimeout": 10, + "HttpsVerifyPeers": true, + "HttpsCACertificates": "", + "UserMetadata": {}, + "UserContentType": {}, + "StableAge": 60, + "StrictAetComparison": false, + "StoreMD5ForAttachments": true, + "LimitFindResults": 0, + "LimitFindInstances": 0, + "LimitJobs": 10, + "LogExportedResources": false, + "KeepAlive": true, + "TcpNoDelay": true, + "HttpThreadsCount": 50, + "StoreDicom": true, + "DicomAssociationCloseDelay": 5, + "QueryRetrieveSize": 10, + "CaseSensitivePN": false, + "LoadPrivateDictionary": true, + "Dictionary": {}, + "SynchronousCMove": true, + "JobsHistorySize": 10, + "SaveJobs": true, + "OverwriteInstances": false, + "MediaArchiveSize": 1, + "StorageAccessOnFind": "Always", + "MetricsEnabled": true, + + "DicomWeb": { + "Enable": true, + "Root": "/dicom-web/", + "EnableWado": true, + "WadoRoot": "/wado", + "Host": "127.0.0.1", + "Ssl": false, + "StowMaxInstances": 10, + "StowMaxSize": 10, + "QidoCaseSensitive": false + } +} diff --git a/docker/OpenResty-Orthanc-Keycloak/docker-compose.yml b/docker/OpenResty-Orthanc-Keycloak/docker-compose.yml new file mode 100644 index 000000000..771737f3c --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/docker-compose.yml @@ -0,0 +1,95 @@ +# Reference: +# - https://docs.docker.com/compose/compose-file +# - https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/ + +version: '3.5' + +services: + # Exposed server that's handling incoming web requests + # Underlying image: openresty/openresty:alpine-fat + ohif_viewer: + build: + # Project root + context: ./../../ + # Relative to context + dockerfile: ./docker/OpenResty-Orthanc-Keycloak/dockerfile + image: webapp:latest + container_name: webapp + volumes: + # Nginx config + - ./config/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro + # Logs + - ./logs/nginx:/var/logs/nginx + # Let's Encrypt + # - letsencrypt_certificates:/etc/letsencrypt + # - letsencrypt_challenges:/var/www/letsencrypt + ports: + - '443:443' # SSL + - '80:80' # Web + depends_on: + - keycloak + - orthanc + restart: on-failure + + # LINK: https://hub.docker.com/r/jodogne/orthanc-plugins/ + # TODO: Update to use Postgres + # https://github.com/mrts/docker-postgresql-multiple-databases + orthanc: + image: jodogne/orthanc-plugins:1.5.6 + hostname: orthanc + container_name: orthanc + volumes: + # Config + - ./config/orthanc.json:/etc/orthanc/orthanc.json:ro + # Persist data + - ./volumes/orthanc-db/:/var/lib/orthanc/db/ + restart: unless-stopped + + # LINK: https://hub.docker.com/r/jboss/keycloak + keycloak: + image: jboss/keycloak:6.0.1 + hostname: keycloak + container_name: keycloak + volumes: + # Theme: https://www.keycloak.org/docs/latest/server_development/index.html#_themes + - ./volumes/keycloak-themes/ohif:/opt/jboss/keycloak/themes/ohif + # Previous Realm Config + - ./config/ohif-keycloak-realm.json:/tmp/ohif-keycloak-realm.json + environment: + # Database + DB_VENDOR: postgres + DB_ADDR: postgres + DB_PORT: 5432 + DB_DATABASE: keycloak + DB_SCHEMA: public + DB_USER: keycloak + DB_PASSWORD: password + # Keycloak + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: password + KEYCLOAK_IMPORT: /tmp/ohif-keycloak-realm.json + # KEYCLOAK_WELCOME_THEME: + # KEYCLOAK_DEFAULT_THEME: + # KEYCLOAK_HOSTNAME: (recommended in prod) + # KEYCLOAK_LOGLEVEL: DEBUG + PROXY_ADDRESS_FORWARDING: 'true' + depends_on: + - postgres + restart: unless-stopped + + # LINK: https://hub.docker.com/_/postgres/ + postgres: + image: postgres:11.2 + hostname: postgres + container_name: postgres + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + restart: unless-stopped + +volumes: + postgres_data: + driver: local diff --git a/docker/OpenResty-Orthanc-Keycloak/dockerfile b/docker/OpenResty-Orthanc-Keycloak/dockerfile new file mode 100644 index 000000000..6019d6790 --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/dockerfile @@ -0,0 +1,65 @@ +# docker-compose +# -------------- +# This dockerfile is used by the `docker-compose.yml` adjacent file. When +# running `docker-compose build`, this dockerfile helps build the "webapp" image. +# All paths are relative to the `context`, which is the project root directory. +# +# docker build +# -------------- +# If you would like to use this dockerfile to build and tag an image, make sure +# you set the context to the project's root directory: +# https://docs.docker.com/engine/reference/commandline/build/ +# +# +# SUMMARY +# -------------- +# This dockerfile has two stages: +# +# 1. Building the React application for production +# 2. Setting up our Nginx (OpenResty*) image w/ step one's output +# +# * OpenResty is functionally identical to Nginx with the addition of Lua out of +# the box. + + +# Stage 1: Build the application +FROM node:11.2.0-slim as builder + +RUN mkdir /usr/src/app +WORKDIR /usr/src/app + +ENV REACT_APP_CONFIG=config/docker_openresty-orthanc-keycloak.js +ENV PATH /usr/src/app/node_modules/.bin:$PATH + +COPY package.json /usr/src/app/package.json +COPY yarn.lock /usr/src/app/yarn.lock + +ADD . /usr/src/app/ +RUN yarn install +RUN yarn run build:web + +# Stage 2: Bundle the built application into a Docker container +# which runs openresty (nginx) using Alpine Linux +# LINK: https://hub.docker.com/r/openresty/openresty +FROM openresty/openresty:1.15.8.1rc1-0-alpine-fat + +RUN mkdir /var/log/nginx +RUN apk add --no-cache openssl +RUN apk add --no-cache openssl-dev +RUN apk add --no-cache git +RUN apk add --no-cache gcc +# !!! +RUN luarocks install lua-resty-openidc + +# +RUN luarocks install lua-resty-jwt +RUN luarocks install lua-resty-session +RUN luarocks install lua-resty-http +# !!! +RUN luarocks install lua-resty-openidc +RUN luarocks install luacrypto + +# Copy build output to image +COPY --from=builder /usr/src/app/build /var/www/html + +ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"] diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/account/.githold b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/account/.githold new file mode 100644 index 000000000..e69de29bb diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/admin/.githold b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/admin/.githold new file mode 100644 index 000000000..e69de29bb diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/email/.githold b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/email/.githold new file mode 100644 index 000000000..e69de29bb diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/resources/css/styles.css b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/resources/css/styles.css new file mode 100644 index 000000000..5b5f0b9be --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/resources/css/styles.css @@ -0,0 +1,212 @@ +body { + background-color: #040507; + background-image: url('../img/background.jpg'); + background-size: cover; + background-repeat: no-repeat; + + width: 100vw; + height: 100vh; + overflow: hidden; + + color: #fff; + font-family: sans-serif; + text-shadow: 0px 0px 10px #000; +} + +a { + color: #fff; +} + +div#kc-content { + position: absolute; + top: 20%; + left: 50%; + width: 550px; + margin-left: -180px; +} + +div#kc-form { + float: left; + width: 350px; +} + +div#kc-form label { + display: block; + font-size: 16px; +} + +div#info-area { + position: fixed; + bottom: 0; + left: 0; + margin-top: 40px; + background-color: rgba(0, 0, 0, 0.4); + padding: 20px; + width: 100%; +} + +div#info-area p { + margin-right: 30px; + display: inline; + text-shadow: none; +} + +input[type='text'], +input[type='password'] { + color: #333; + font-size: 18px; + margin-bottom: 20px; + background-color: rgba(256, 256, 256, 0.7); + border: 0px solid rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 2px 2px rgba(0, 0, 0, 0.15); + padding: 10px; + width: 296px; +} + +input[type='text']:hover, +input[type='password']:hover { + background-color: rgba(256, 256, 256, 0.9); +} + +input[type='submit'] { + border: none; + + background: -webkit-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ); + background: -moz-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ); + background: -ms-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ); + background: -o-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ); + + box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.5); + + color: rgba(0, 0, 0, 0.6); + + font-size: 14px; + font-weight: bold; + + padding: 10px; + margin-top: 20px; + margin-right: 10px; + width: 150px; +} + +input[type='submit']:hover { + background-color: rgba(255, 255, 255, 0.8); +} + +div#kc-form-options div { + display: inline-block; + margin-right: 20px; + font-size: 12px; +} + +div#kc-form-options div label { + font-size: 12px; +} + +div#kc-feedback { + box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.5); + position: fixed; + top: 0; + left: 0; + width: 100%; + text-align: center; +} + +div#kc-feedback-wrapper { + padding: 1em; +} + +div.feedback-success { + background-color: rgba(155, 155, 255, 0.1); +} + +div.feedback-warning { + background-color: rgba(255, 175, 0, 0.1); +} + +div.feedback-error { + background-color: rgba(255, 0, 0, 0.1); +} + +div#kc-header { + display: none; +} + +div#kc-registration { + margin-bottom: 20px; +} + +div#social-login { + border-left: 1px solid rgba(255, 255, 255, 0.2); + float: right; + width: 150px; + padding: 20px 0 200px 40px; +} + +div.social-login span { + display: none; +} + +div#kc-social-providers ul { + list-style: none; + margin: 0; + padding: 0; +} + +div#kc-social-providers ul li { + margin-bottom: 20px; +} + +div#kc-social-providers ul li span { + display: inline; + width: 100px; +} + +a.zocial { + border: none; + background: -webkit-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ) !important; + background: -moz-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ) !important; + background: -ms-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ) !important; + background: -o-linear-gradient( + top, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.1) + ) !important; + box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.5); + color: rgba(0, 0, 0, 0.6); + width: 130px; + text-shadow: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; +} diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/resources/img/background.jpg b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/resources/img/background.jpg new file mode 100644 index 000000000..9d4f940bd Binary files /dev/null and b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/resources/img/background.jpg differ diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/theme.properties b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/theme.properties new file mode 100644 index 000000000..512f13be2 --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/login/theme.properties @@ -0,0 +1,3 @@ +parent=base +import=common/keycloak +styles=lib/zocial/zocial.css css/styles.css diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/welcome/.githold b/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/ohif/welcome/.githold new file mode 100644 index 000000000..e69de29bb diff --git a/docker/OpenResty-Orthanc-Keycloak/volumes/orthanc-db/.gitignore b/docker/OpenResty-Orthanc-Keycloak/volumes/orthanc-db/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/docker/OpenResty-Orthanc-Keycloak/volumes/orthanc-db/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docker/docker-compose-orthanc.yml b/docker/docker-compose-orthanc.yml deleted file mode 100644 index 5b4edea0e..000000000 --- a/docker/docker-compose-orthanc.yml +++ /dev/null @@ -1,39 +0,0 @@ -# docker-compose: -# https://docs.docker.com/compose/overview/ - -version: '3.5' - -services: - orthanc: - container_name: orthanc - image: jodogne/orthanc-plugins - volumes: - - ../sampledata:/sampledata - ports: - - '4242:4242' - - '8042:8042' - restart: always - viewer: - container_name: ohif-viewer - build: - # Up to the root directory - context: ../ - # Use our dockerfile to build the viewer - dockerfile: dockerfile - ports: - - '80:80' - depends_on: - - orthanc - environment: - - NODE_ENV=production - - REACT_APP_CONFIG=config/local_orthanc.js - restart: always - volumes: - - ../config/local_orthanc.js:/usr/share/nginx/html/servers.js - proxy: - image: nginx - restart: always - ports: - - 8899:80 - volumes: - - ./nginx-proxy/conf/nginx.conf:/etc/nginx/nginx.conf:ro diff --git a/dockerfile b/dockerfile-web similarity index 100% rename from dockerfile rename to dockerfile-web diff --git a/dockersupport/app.json b/dockersupport/app.json deleted file mode 100644 index 1d5ab185c..000000000 --- a/dockersupport/app.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "apps" : [{ - "name" : "ohif-viewer", - "script" : "main.js", - "watch" : true, - "merge_logs" : true, - "cwd" : "/app/bundle/", - "env": { - "METEOR_SETTINGS": { - "servers": { - "dicomWeb": [ - { - "name": "Orthanc", - "wadoUriRoot": "http://pacsIP:8042/wado", - "qidoRoot": "http://pacsIP:8042/dicom-web", - "wadoRoot": "http://pacsIP:8042/dicom-web", - "qidoSupportsIncludeField": false, - "imageRendering": "wadouri", - "thumbnailRendering": "wadouri", - "requestOptions": { - "auth": "orthanc:orthanc", - "logRequests": true, - "logResponses": false, - "logTiming": true - } - } - ] - }, - "defaultServiceType": "dicomWeb", - "public": { - "ui": { - "studyListDateFilterNumDays": 1 - } - }, - "proxy": { - "enabled": true - } - } - } - }] -} \ No newline at end of file diff --git a/dockersupport/settings.json b/dockersupport/settings.json deleted file mode 100644 index c54e5c5d6..000000000 --- a/dockersupport/settings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ -"servers": { - "dicomWeb": [ - { - "name": "Orthanc", - "wadoUriRoot": "http://pacsIP:8042/wado", - "qidoRoot": "http://pacsIP:8042/dicom-web", - "wadoRoot": "http://pacsIP:8042/dicom-web", - "qidoSupportsIncludeField": false, - "imageRendering": "wadouri", - "thumbnailRendering": "wadouri", - "requestOptions": { - "auth": "orthanc:orthanc", - "logRequests": true, - "logResponses": false, - "logTiming": true - } - } - ] - }, - "defaultServiceType": "dicomWeb", - "public": { - "ui": { - "studyListDateFilterNumDays": 1 - } - }, - "proxy": { - "enabled": true - } -} \ No newline at end of file diff --git a/docs/latest/README.md b/docs/latest/README.md index 60c1b8a90..e97410df6 100644 --- a/docs/latest/README.md +++ b/docs/latest/README.md @@ -1,11 +1,44 @@ -##### Looking for your Deploy Preview? - Deploy Preview for Viewer +##### Looking for a Deploy Preview? - Deploy Preview for Viewer -# Introduction - test +> ATTENTION! You are looking at the docs for the `React` version of the OHIF +> Viewer. If you're looking for the `Meteor` version's documentation (now +> deprecated), select it's version from the dropdown box in the top left corner +> of this page. -The [Open Health Imaging Foundation](https://www.ohif.org) is developing an open source framework for constructing web-based medical imaging applications. +# Introduction -## The **OHIF Viewer**: A general purpose DICOM Viewer ([demo](http://viewer.ohif.org/)) +The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source, +web-based, medical imaging viewer. It can be configured to connect to Image +Archives that support [DicomWeb][dicom-web], and offers support for mapping to +proprietary API formats. OHIF maintained extensions add support for viewing, +annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes). ![OHIF Viewer Screenshot](../assets/img/viewer.png) -The Open Health Imaging Foundation intends to provide a simple general purpose DICOM Viewer which can be easily extended for specific uses. +
The OHIF Viewer: A general purpose DICOM Viewer (Live Demo)
+ +The Open Health Imaging Foundation intends to provide a simple general purpose +DICOM Viewer which can be easily extended for specific uses. If you find +yourself unable to extend the viewer for your purposes, please reach out via our +[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve +our integration and extension points. + +## Where to Next? + +Check out these helpful links: + +- Ready to dive into some code? Check out our + [Getting Started Guide](./essentials/getting-started.md). +- We're an active, vibrant community. + [Learn how you can be more involved.](./contributing.md) +- Feeling lost? Read our [help page](./help.md). + + + + +[ohif-org]: https://www.ohif.org +[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb +[gh-issues]: https://github.com/OHIF/Viewers/issues + diff --git a/docs/latest/SUMMARY.md b/docs/latest/SUMMARY.md index 7ed455dd4..1e8e13635 100644 --- a/docs/latest/SUMMARY.md +++ b/docs/latest/SUMMARY.md @@ -2,6 +2,7 @@ - Essentials - [Getting Started](essentials/getting-started.md) + - [Data Source](essentials/data-source.md) - [Configuration](essentials/configuration.md) - [Themeing](essentials/themeing.md) - [Troubleshooting](essentials/troubleshooting.md) @@ -41,11 +42,8 @@ - Stand-Alone - [Build for Production](deployment/recipes/build-for-production.md) - [Static](deployment/recipes/static-assets.md) - - [Docker]() - - [Nginx + Orthanc]() - - [Nginx + dcm4chee]() - - [Nginx + DICOMCloud]() - - [User Access Control]() + - [Nginx + Image Archive](deployment/recipes/nginx--image-archive.md) + - [User Account Control](deployment/recipes/user-account-control.md) --- diff --git a/img/designs/architecture-diagram b/docs/latest/assets/designs/architecture-diagram similarity index 100% rename from img/designs/architecture-diagram rename to docs/latest/assets/designs/architecture-diagram diff --git a/img/designs/cloud.svg b/docs/latest/assets/designs/cloud.svg similarity index 100% rename from img/designs/cloud.svg rename to docs/latest/assets/designs/cloud.svg diff --git a/img/designs/embedded-viewer-diagram b/docs/latest/assets/designs/embedded-viewer-diagram similarity index 100% rename from img/designs/embedded-viewer-diagram rename to docs/latest/assets/designs/embedded-viewer-diagram diff --git a/img/designs/npm-logo-red.svg b/docs/latest/assets/designs/npm-logo-red.svg similarity index 100% rename from img/designs/npm-logo-red.svg rename to docs/latest/assets/designs/npm-logo-red.svg diff --git a/docs/latest/assets/designs/user-access-control-request-flow.fig b/docs/latest/assets/designs/user-access-control-request-flow.fig new file mode 100644 index 000000000..17d278d90 Binary files /dev/null and b/docs/latest/assets/designs/user-access-control-request-flow.fig differ diff --git a/docs/latest/assets/img/jwt-explained.png b/docs/latest/assets/img/jwt-explained.png new file mode 100644 index 000000000..f26509a16 Binary files /dev/null and b/docs/latest/assets/img/jwt-explained.png differ diff --git a/docs/latest/assets/img/keycloak-default-theme.png b/docs/latest/assets/img/keycloak-default-theme.png new file mode 100644 index 000000000..0ea77f965 Binary files /dev/null and b/docs/latest/assets/img/keycloak-default-theme.png differ diff --git a/docs/latest/assets/img/keycloak-ohif-theme.png b/docs/latest/assets/img/keycloak-ohif-theme.png new file mode 100644 index 000000000..ad060f262 Binary files /dev/null and b/docs/latest/assets/img/keycloak-ohif-theme.png differ diff --git a/docs/latest/assets/img/user-access-control-request-flow.png b/docs/latest/assets/img/user-access-control-request-flow.png new file mode 100644 index 000000000..573c83503 Binary files /dev/null and b/docs/latest/assets/img/user-access-control-request-flow.png differ diff --git a/docs/latest/deployment/_user-account-control-flow-diagram.md b/docs/latest/deployment/_user-account-control-flow-diagram.md new file mode 100644 index 000000000..b6937cd6b --- /dev/null +++ b/docs/latest/deployment/_user-account-control-flow-diagram.md @@ -0,0 +1,4 @@ +
+ request flow example +
simplified request flow diagram
+
diff --git a/docs/latest/deployment/recipes/nginx--image-archive.md b/docs/latest/deployment/recipes/nginx--image-archive.md new file mode 100644 index 000000000..0027e8aa7 --- /dev/null +++ b/docs/latest/deployment/recipes/nginx--image-archive.md @@ -0,0 +1 @@ +# Nginx + Image Archive diff --git a/docs/latest/deployment/recipes/user-account-control.md b/docs/latest/deployment/recipes/user-account-control.md new file mode 100644 index 000000000..d22648a60 --- /dev/null +++ b/docs/latest/deployment/recipes/user-account-control.md @@ -0,0 +1,289 @@ +# User Account Control + +> DISCLAIMER! We make no claims or guarantees of this approach's security. If in +> doubt, enlist the help of an expert and conduct proper audits. + +Making a viewer and its medical imaging data accessible on the open web can +provide a lot of benefits, but requires additional security to make sure +sensitive information can only be viewed by authorized individuals. Most image +archives are equipped with basic security measures, but they are not +robust/secure enough for the open web. + +This guide covers one of many potential production setups that secure our +sensitive data. + +## Overview + +This guide builds on top of our +[Nginx + Image Archive guide](/deployment/recipes/nginx--image-archive.md), +wherein we used a [`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) +to retrieve resources from our image archive (Orthanc). + +To add support for "User Account Control" we introduce +[Keycloak](https://www.keycloak.org/about.html). Keycloak is an open source +Identity and Access Management solution that makes it easy to secure +applications and services with little to no code. We improve upon our +`reverse proxy` setup by integrating Keycloak and Nginx to create an +`authenticating reverse proxy`. + +> An authenticating reverse proxy is a reverse proxy that only retrieves the +> resources on behalf of a client if the client has been authenticated. If a +> client is not authenticated they can be redirected to a login page. + +This setup allows us to create a setup similar to the one pictured below: + +{% include "./../_user-account-control-flow-diagram.md" %} + +- All web requests are routed through `nginx` on our `OpenResty` image +- `/pacs` is a reverse proxy for `orthanc`'s `DICOM Web` endpoints + - Requires valid `Authorization: Bearer ` header +- `/pacs-admin` is a reverse proxy for `orthanc`'s Web Admin +- `/auth` is a reverse proxy for `keycloak` +- All static resources for OHIF Viewer are unprotected and accessible. We have + application logic that will redirect unauthenticated users to the appropriate + `keycloak` login screen. + +## Getting Started + +### Requirements + +- Docker + - [Docker for Mac](https://docs.docker.com/docker-for-mac/) + - [Docker for Windows](https://docs.docker.com/docker-for-windows/) + +_Not sure if you have `docker` installed already? Try running `docker --version` +in command prompt or terminal_ + +### Setup + +_Spin Things Up_ + +- Navigate to `/docker/OpenResty-Orthanc-Keycloak` in your shell +- Run `docker-compose up` + +_Create Your First User_ + +- Navigate to: `http://127.0.0.1/auth/admin` +- Sign in with: `admin`/`password` +- From the top left dropdown, select the `Ohif` realm +- From the left sidebar, under `Manage`, select `Users` +- Click `Add User` + - Username: `test` + - Email Verified: `ON` + - Click `Save` +- Click the `Credentials` Tab + - New Pasword: `test` + - Password Confirmation: `test` + - Temporary: `OFF` + - Click: `Reset Password` +- From the top right dropdown, select `Admin`, then `Sign Out` + +_Sign In_ + +- Navigate to `http://127.0.0.1/` +- Username: `test`, Password: `test` +- Click `Log In` + +_Upload Your First Study_ + +- Navigate to `http://127.0.0.1/pacs-admin` +- If you're not already logged in, use `test`/`test` +- From the top right, select "Upload" +- Click "Select files to upload..." (DICOM) +- Click "Start the upload" +- Navigate back to `http://127.0.0.1/` to view your studies in the Study List + +### Troubleshooting + +_Exit code 137_ + +This means Docker ran out of memory. Open Docker Desktop, go to the `advanced` +tab, and increase the amount of Memory available. + +_Cannot create container for service X_ + +Use this one with caution: `docker system prune` + +_X is already running_ + +Stop running all containers: + +- Win: `docker ps -a -q | ForEach { docker stop $_ }` +- Linux: `docker stop $(docker ps -a -q)` + +### Configuration + +After verifying that everything runs with default configuration values, you will +likely want to update: + +- The domain: `http://127.0.0.1` +- Set secure, non-default passwords +- Regenerate Keycloak Client Secrets + +#### OHIF Viewer + +The OHIF Viewer's configuration is imported from a static `.js` file and made +available globally at `window.config`. The configuration we use is set to a +specific file when we build the viewer, and determined by the env variable: +`REACT_APP_CONFIG`. You can see where we set its value in the `dockerfile` for +this solution: + +`ENV REACT_APP_CONFIG=config/docker_openresty-orthanc-keycloak.js` + +You can find the configuration we're using here: +`/public/config/docker_openresty-orthanc-keycloak.js` + +To rebuild the `webapp` image created by our `dockerfile` after updating the +Viewer's configuration, you can run: + +- `docker-compose build` OR +- `docker-compose up --build` + +#### Other + +All other files are found in: `/docker/OpenResty-Orthanc-Keycloak/` + +| Service | Configuration | Docs | +| ----------------- | ------------------------------------------------ | ------------------------------------------- | +| OHIF Viewer | [dockerfile][dockerfile] / [config.js][config] | You're reading them now! | +| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] | +| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] | +| Keycloak | [`/ohif-keycloak-realm.json`][config-keycloak]\* | | + +- \* These are the seed values for Keycloak. They can be manually updated at + `http://127.0.0.1/auth/admin` + +#### Keycloak Themeing + +The `Login` screen for the `ohif-viewer` client is using a Custom Keycloak +theme. You can find the source files for it in +`/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/`. You can see how +we add it to Keycloak in the `docker-compose` file, and you can read up on how +to leverage custom themes in +[Keycloak's own docs](https://www.keycloak.org/docs/latest/server_development/index.html#_themes). + +| Default Theme | OHIF Theme | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------- | +| ![Keycloak Default Theme](../../assets/img/keycloak-default-theme.png) | ![Keycloak OHIF Theme](../../assets/img/keycloak-ohif-theme.png) | + +## Next Steps + +### Deploying to Production + +While these configuration and docker-compose files model an environment suitable +for production, they are not easy to deploy "as is". You can either: + +- Manually recreate this environment and deploy built application files **OR** +- Deploy to a cloud kubernetes provider like + [Digital Ocean](https://www.digitalocean.com/products/kubernetes/) **OR** + - [See a full list of cloud providers here](https://landscape.cncf.io/category=cloud&format=card-mode&grouping=category) +- Find and follow your preferred provider's guide on setting up + [swarms and stacks](https://docs.docker.com/get-started/) + +### Adding SSL + +Adding SSL registration and renewal for your domain with Let's Encrypt that +terminates at Nginx is an incredibly important step toward securing your data. +Here are some resources, specific to this setup, that may be helpful: + +- [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl) +- [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/) + +While we terminate SSL at Nginx, it may be worth using self signed certificates +for communication between services. + +- [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/) + +### Use PostgresSQL w/ Orthanc + +Orthanc can handle a large amount of data and requests, but if you find that +requests start to slow as you add more and more studies, you may want to +configure your Orthanc instance to use PostgresSQL. Instructions on how to do +that can be found in the +[`Orthanc Server Book`](http://book.orthanc-server.com/users/docker.html), under +"PostgreSQL and Orthanc inside Docker" + +### Improving This Guide + +Here are some improvements this guide would benefit from, and that we would be +more than happy to accept Pull Requests for: + +- SSL Support +- Complete configuration with `.env` file (or something similar) +- Keycloak Theme improvements +- Any security issues +- One-click deploy to a cloud provider + +## Resources + +### Misc. Helpful Commands + +_Check if `nginx.conf` is valid:_ + +```bash +docker run --rm -t -a stdout --name my-openresty -v $PWD/config/:/usr/local/openresty/nginx/conf/:ro openresty/openresty:alpine-fat openresty -c /usr/local/openresty/nginx/conf/nginx.conf -t +``` + +_Interact w/ running container:_ + +`docker exec -it CONTAINER_NAME bash` + +_List running containers:_ + +`docker ps` + +_Clear Keycloak DB so you can re-seed values:_ + +- `docker volume prune` OR +- `docker volume ls` and `docker volume rm VOLUME_NAME VOLUME_NAME` + +### Referenced Articles + +The inspiration for our setup was driven largely by these articles: + +- [Securing Nginx with Keycloak](https://edhull.co.uk/blog/2018-06-06/keycloak-nginx) +- [Authenticating Reverse Proxy with Keycloak](https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/) +- [Securing APIs with Kong and Keycloak](https://www.jerney.io/secure-apis-kong-keycloak-1/) + +For more documentation on the software we've chosen to use, you may find the +following resources helpful: + +- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html) +- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/) +- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/) +- [Auth0: Picking a Grant Type](https://auth0.com/docs/api-auth/which-oauth-flow-to-use) + +We chose to use a generic OpenID Connect library on the client, but it's worth +noting that Keycloak comes packaged with its own: + +- [redux-oidc](https://github.com/maxmantz/redux-oidc) (Which wraps + [oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki)) +- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter) + +If you're not already drowning in links, here are some good security resources +for OAuth: + +- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660) +- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code) + - [Good description on SSO Protocols](https://www.keycloak.org/docs/2.5/server_admin/topics/sso-protocols/oidc.html) + +For a different take on this setup, check out the repository one of our +community members put together: + +- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker) + + + + + +[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration +[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc + +[config]: # +[dockerfile]: # +[config-nginx]: # +[config-orthanc]: # +[config-keycloak]: # + diff --git a/docs/latest/essentials/architecture.md b/docs/latest/essentials/architecture.md index 8675c14ce..7b8465a5b 100644 --- a/docs/latest/essentials/architecture.md +++ b/docs/latest/essentials/architecture.md @@ -2,14 +2,25 @@ The ohif-viewer package provides two different build processes: -# create-react-app +## create-react-app -> [create-react-app](https://github.com/facebook/create-react-app) provides pre-configured build process for developing front-end applications with [React](https://reactjs.org/). +> [create-react-app](https://github.com/facebook/create-react-app) provides +> pre-configured build process for developing front-end applications with +> [React](https://reactjs.org/). -The ohif-viewer package can be run as a create-react-app application. This is useful for development, debugging, or evolving ohif-viewer into your own custom imaging application. +The ohif-viewer package can be run as a create-react-app application. This is +useful for development, debugging, or evolving ohif-viewer into your own custom +imaging application. -# Rollup +## Rollup -> [Rollup](https://rollupjs.org/guide/en) is a module bundler for JavaScript. It uses the new standardized format for code modules included in the ES6 revision of JavaScript. +> [Rollup](https://rollupjs.org/guide/en) is a module bundler for JavaScript. It +> uses the new standardized format for code modules included in the ES6 revision +> of JavaScript. -The ohif-viewer package can be built with Rollup to provide a set of React components which can be dropped into a larger application. Specifically, the ohif-viewer package provides a React component named `OHIFViewer` which is the entire viewer, configurable via React `props`. This is useful for including the OHIF Viewer in a larger web application, as the entire application can be provided via a `