diff --git a/.env b/.env index 16317dd..dabc5fb 100644 --- a/.env +++ b/.env @@ -1,4 +1,105 @@ +# ============================================================================ +# KEYCLOAK OIDC AUTHENTICATION CONFIGURATION +# ============================================================================ +# Uses Keycloak as the identity provider for OIDC authentication +# Keycloak Admin: http://localhost:8080/auth/admin (keycloak / password) + +# Project Configuration +COMPOSE_PROJECT_NAME=oasispythonui +OASIS_DEBUG=1 + +# Docker socket path (Docker Desktop uses ~/.docker/desktop/docker.sock) +# Standard Docker: /var/run/docker.sock +DOCKER_SOCK=/var/run/docker.sock + +# Hostname Configuration +OASIS_UI_HOSTNAME=ui.oasis.local +OASIS_PROTOCOL=http + +# Authentication Type +API_AUTH_TYPE=keycloak +OASIS_SERVER_ALLOWED_OIDC_AUTH_PROVIDERS=keycloak,authentik + +# Image Versions SERVER_IMG=coreoasis/api_server +VERS_API=2.5 WORKER_IMG=coreoasis/model_worker -SCENARIOS_UI_IMG=coreoasis/oasis_scenarios -SCENARIOS_PATH=../Scenarios +VERS_WORKER=2.5 +PYTHONUI_IMG=coreoasis/oasispythonui_app +VERS_UI=latest +VERS_PIWIND=stable/2.5.x + +# Database Configuration +OASIS_SERVER_DB_HOST=server-db +OASIS_SERVER_DB_PORT=5432 +OASIS_SERVER_DB_NAME=oasis +OASIS_SERVER_DB_USER=oasis +OASIS_SERVER_DB_PASS=oasis + +OASIS_CELERY_DB_HOST=celery-db +OASIS_CELERY_DB_PORT=5432 +OASIS_CELERY_DB_NAME=celery +OASIS_CELERY_DB_USER=celery +OASIS_CELERY_DB_PASS=password + +# Broker & Channel Layer +RABBITMQ_DEFAULT_USER=rabbit +RABBITMQ_DEFAULT_PASS=rabbit +OASIS_CELERY_BROKER_URL=amqp://rabbit:rabbit@broker:5672 +REDIS_HOST=channel-layer +REDIS_PORT=6379 +OASIS_SERVER_CHANNEL_LAYER_SSL=false + +# ============================================================================ +# KEYCLOAK CONFIGURATION +# ============================================================================ + +# Keycloak Service +KEYCLOAK_HOST=keycloak +KEYCLOAK_PORT=8080 + +# Keycloak Admin Console Credentials +KEYCLOAK_ADMIN_USER=keycloak +KEYCLOAK_ADMIN_PASSWORD=password + +# Keycloak Database +KEYCLOAK_DB_NAME=keycloak +KEYCLOAK_DB_USER=keycloak +KEYCLOAK_DB_PASSWORD=password + +# OIDC Client Configuration +# These match the realm configuration in oidc/keycloak/oasis-realm.json.template +OIDC_KEYCLOAK_CLIENT_NAME=oasis-server +OIDC_KEYCLOAK_CLIENT_SECRET=e4f4fb25-2250-4210-a7d6-9b16c3d2ab77 + +# Service Account Client (for service-to-service auth) +OASIS_SERVICE_CLIENT_NAME=oasis-service +OASIS_SERVICE_CLIENT_SECRET=serviceNotSoSecret + +# Advanced Configuration +OASIS_PORTFOLIO_UPLOAD_VALIDATION=0 +OASIS_OASISLMF_VERSION= +OASIS_ODS_VERSION= +OASIS_ODM_VERSION= +OASIS_OED_SCHEMA_INFO= + +# ============================================================================ +# User Configuration: +# - Default users defined in: oidc/keycloak/users.yaml +# - Edit that file to add/modify users +# - Users: admin (admin), user (non-admin) +# +# Quick Start: +# 1. cp .env.keycloak .env +# 2. ./install.sh +# 3. Wait for Keycloak to start (can take 2-3 minutes first time) +# 4. Access Keycloak Admin: http://localhost:8080/auth/admin +# 5. Access UI: http://localhost:8501 +# 6. Login via Keycloak +# +# OIDC Endpoints (routed through traefik on port 80): +# - Authorization: http://localhost/auth/realms/oasis/protocol/openid-connect/auth +# - Token: http://localhost/auth/realms/oasis/protocol/openid-connect/token +# - UserInfo: http://localhost/auth/realms/oasis/protocol/openid-connect/userinfo +# - Keycloak Admin (direct): http://localhost:8080/auth/admin +# ============================================================================ diff --git a/.env.authentik b/.env.authentik new file mode 100644 index 0000000..4422e14 --- /dev/null +++ b/.env.authentik @@ -0,0 +1,116 @@ +# ============================================================================ +# AUTHENTIK OIDC AUTHENTICATION CONFIGURATION +# ============================================================================ +# Uses Authentik as the identity provider for OIDC authentication +# Authentik Admin: http://localhost:9000/authentik/if/admin (akadmin / password) + +# Project Configuration +COMPOSE_PROJECT_NAME=oasispythonui +OASIS_DEBUG=1 + +# Docker socket path (Docker Desktop uses ~/.docker/desktop/docker.sock) +# Standard Docker: /var/run/docker.sock +DOCKER_SOCK=/var/run/docker.sock + +# Hostname Configuration +OASIS_UI_HOSTNAME=ui.oasis.local +OASIS_PROTOCOL=http + +# Authentication Type +API_AUTH_TYPE=authentik +OASIS_SERVER_ALLOWED_OIDC_AUTH_PROVIDERS=keycloak,authentik + +# Image Versions +SERVER_IMG=coreoasis/api_server +VERS_API=2.5 +WORKER_IMG=coreoasis/model_worker +VERS_WORKER=2.5 +PYTHONUI_IMG=coreoasis/oasispythonui_app +VERS_UI=latest +VERS_PIWIND=stable/2.5.x + +# Database Configuration +OASIS_SERVER_DB_HOST=server-db +OASIS_SERVER_DB_PORT=5432 +OASIS_SERVER_DB_NAME=oasis +OASIS_SERVER_DB_USER=oasis +OASIS_SERVER_DB_PASS=oasis + +OASIS_CELERY_DB_HOST=celery-db +OASIS_CELERY_DB_PORT=5432 +OASIS_CELERY_DB_NAME=celery +OASIS_CELERY_DB_USER=celery +OASIS_CELERY_DB_PASS=password + +# Broker & Channel Layer +RABBITMQ_DEFAULT_USER=rabbit +RABBITMQ_DEFAULT_PASS=rabbit +OASIS_CELERY_BROKER_URL=amqp://rabbit:rabbit@broker:5672 +REDIS_HOST=channel-layer +REDIS_PORT=6379 +OASIS_SERVER_CHANNEL_LAYER_SSL=false + +# ============================================================================ +# AUTHENTIK CONFIGURATION +# ============================================================================ + +# Authentik Service +AUTHENTIK_HOST=authentik +AUTHENTIK_PORT=9000 + +# Authentik Bootstrap Configuration (initial setup) +AUTHENTIK_BOOTSTRAP_USER=akadmin +AUTHENTIK_BOOTSTRAP_EMAIL=akadmin@example.com +AUTHENTIK_BOOTSTRAP_PASSWORD=password +AUTHENTIK_BOOTSTRAP_TOKEN=my-demo-token-abc123 + +# Authentik Secret Key (for encryption) +# CHANGE THIS IN PRODUCTION! +AUTHENTIK_SECRET_KEY=notsosecretkey + +# Authentik Database +AUTHENTIK_DB_NAME=authentik +AUTHENTIK_DB_USER=authentik +AUTHENTIK_DB_PASSWORD=password + +# OIDC Client Configuration +# These match the blueprint configuration in oidc/authentik/oasis-blueprint.yaml.template +OIDC_AUTHENTIK_CLIENT_NAME=oasis-server +OIDC_AUTHENTIK_CLIENT_SECRET=EfNMUM3GG1bd1CYUvNfiBGWKfvbGFiNAdutEqHSarZ9H7oL0sZfKLvPT1ujaqVm2839Vka8Ky0elliMQ6yWKN8Jv8dzh3BeVFn0F7LPquGkIus6JJ9nGH1vtfCt7AhtO + +# Service Account Client (for service-to-service auth) +OASIS_SERVICE_CLIENT_NAME=oasis-service +OASIS_SERVICE_CLIENT_SECRET=serviceNotSoSecret + +# Advanced Configuration +OASIS_PORTFOLIO_UPLOAD_VALIDATION=0 +OASIS_OASISLMF_VERSION= +OASIS_ODS_VERSION= +OASIS_ODM_VERSION= +OASIS_OED_SCHEMA_INFO= + +# ============================================================================ +# User Configuration: +# - Default users defined in: oidc/authentik/users.yaml +# - Edit that file to add/modify users +# - Users: admin (admin), user (non-admin) +# +# Quick Start: +# 1. cp .env.authentik .env +# 2. ./install.sh +# 3. Wait for Authentik to start (can take 2-3 minutes first time) +# 4. Access Authentik Admin: http://localhost:9000/authentik/if/admin +# 5. Access UI: http://localhost:8501 +# 6. Login via Authentik +# +# OIDC Endpoints (routed through traefik on port 80): +# - Authorization: http://localhost/authentik/application/o/authorize/ +# - Token: http://localhost/authentik/application/o/token/ +# - UserInfo: http://localhost/authentik/application/o/userinfo/ +# - Authentik Admin (direct): http://localhost:9000/authentik/if/admin +# +# Security Notes: +# - AUTHENTIK_SECRET_KEY must be changed in production! +# - This key is used for encrypting sensitive data +# - Generate a secure random key for production use +# ============================================================================ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7cf4e43 --- /dev/null +++ b/.env.example @@ -0,0 +1,193 @@ +# ============================================================================ +# OASIS PYTHONUI DOCKER COMPOSE CONFIGURATION +# ============================================================================ +# Copy this file to .env and configure for your deployment +# For quick start, use one of the example files: +# - .env.simple (Simple JWT authentication) +# - .env.keycloak (Keycloak OIDC) +# - .env.authentik (Authentik OIDC) + +# ============================================================================ +# PROJECT CONFIGURATION +# ============================================================================ + +# Docker Compose project name (used for volume and network naming) +COMPOSE_PROJECT_NAME=oasispythonui + +# Enable debug mode (1=on, 0=off) +OASIS_DEBUG=1 + +# Docker socket path (required for traefik reverse proxy) +# Standard Docker: /var/run/docker.sock +# Docker Desktop: ~/.docker/desktop/docker.sock +DOCKER_SOCK=/var/run/docker.sock + +# ============================================================================ +# HOSTNAME CONFIGURATION +# ============================================================================ +# Matches OasisPlatform Kubernetes values.yaml: ingress.uiHostname +# +# For local development: localhost +# For production/custom domain: ui.oasis.local or your domain +# This hostname is used in: +# - OIDC redirect URIs +# - API endpoint URLs +# - External access URLs + +OASIS_UI_HOSTNAME=localhost +OASIS_PROTOCOL=http + +# ============================================================================ +# AUTHENTICATION CONFIGURATION +# ============================================================================ + +# Authentication type +# Options: simple, keycloak, authentik +API_AUTH_TYPE=simple + +# Allowed OIDC providers (comma-separated) +# Used when API_AUTH_TYPE is keycloak or authentik +OASIS_SERVER_ALLOWED_OIDC_AUTH_PROVIDERS=keycloak,authentik + +# ============================================================================ +# SIMPLE AUTHENTICATION (when API_AUTH_TYPE=simple) +# ============================================================================ +# These credentials are used for: +# - Django admin interface +# - REST API JWT authentication +# - Service-to-service communication + +OASIS_ADMIN_USER=admin +OASIS_ADMIN_PASS=password + +# ============================================================================ +# OIDC SERVICE ACCOUNT (when API_AUTH_TYPE=keycloak or authentik) +# ============================================================================ +# OIDC client credentials for service-to-service authentication. +# Must match the service client configured in Keycloak/Authentik. +# Not used for simple auth (install.sh uses OASIS_ADMIN_USER/PASS instead). + +OASIS_SERVICE_CLIENT_NAME=oasis-service +OASIS_SERVICE_CLIENT_SECRET=serviceNotSoSecret + +# ============================================================================ +# IMAGE VERSIONS +# ============================================================================ + +SERVER_IMG=coreoasis/api_server +VERS_API=2.5 + +WORKER_IMG=coreoasis/model_worker +VERS_WORKER=2.5 + +PYTHONUI_IMG=coreoasis/oasispythonui_app +VERS_UI=latest + +# PiWind model version +VERS_PIWIND=stable/2.5.x + +# ============================================================================ +# DATABASE CONFIGURATION +# ============================================================================ + +# Oasis API Database (PostgreSQL) +OASIS_SERVER_DB_HOST=server-db +OASIS_SERVER_DB_PORT=5432 +OASIS_SERVER_DB_NAME=oasis +OASIS_SERVER_DB_USER=oasis +OASIS_SERVER_DB_PASS=oasis + +# Celery Results Database (PostgreSQL) +OASIS_CELERY_DB_HOST=celery-db +OASIS_CELERY_DB_PORT=5432 +OASIS_CELERY_DB_NAME=celery +OASIS_CELERY_DB_USER=celery +OASIS_CELERY_DB_PASS=password + +# ============================================================================ +# BROKER & CHANNEL LAYER +# ============================================================================ + +# RabbitMQ Message Broker +RABBITMQ_DEFAULT_USER=rabbit +RABBITMQ_DEFAULT_PASS=rabbit +OASIS_CELERY_BROKER_URL=amqp://rabbit:rabbit@broker:5672 + +# Redis/Valkey Channel Layer +REDIS_HOST=channel-layer +REDIS_PORT=6379 +OASIS_SERVER_CHANNEL_LAYER_SSL=false + +# ============================================================================ +# KEYCLOAK CONFIGURATION (when API_AUTH_TYPE=keycloak) +# ============================================================================ + +# Keycloak Service Configuration +KEYCLOAK_HOST=keycloak +KEYCLOAK_PORT=8080 + +# Keycloak Admin Console Credentials +# Access at: http://localhost:8080/auth/admin +KEYCLOAK_ADMIN_USER=keycloak +KEYCLOAK_ADMIN_PASSWORD=password + +# Keycloak Database +KEYCLOAK_DB_NAME=keycloak +KEYCLOAK_DB_USER=keycloak +KEYCLOAK_DB_PASSWORD=password + +# OIDC Client Configuration +# These must match the client configurations in the realm template +OIDC_KEYCLOAK_CLIENT_NAME=oasis-server +OIDC_KEYCLOAK_CLIENT_SECRET=e4f4fb25-2250-4210-a7d6-9b16c3d2ab77 + +# Default Users Configuration +# Users are defined in: oidc/keycloak/users.yaml +# Edit that file to add/modify users + +# ============================================================================ +# AUTHENTIK CONFIGURATION (when API_AUTH_TYPE=authentik) +# ============================================================================ + +# Authentik Service Configuration +AUTHENTIK_HOST=authentik +AUTHENTIK_PORT=9000 + +# Authentik Bootstrap Configuration +# Used for initial setup only +# Access at: http://localhost:9000/authentik/if/admin +AUTHENTIK_BOOTSTRAP_USER=akadmin +AUTHENTIK_BOOTSTRAP_EMAIL=akadmin@example.com +AUTHENTIK_BOOTSTRAP_PASSWORD=password +AUTHENTIK_BOOTSTRAP_TOKEN=my-demo-token-abc123 + +# Authentik Secret Key (for encryption) +# CHANGE THIS IN PRODUCTION! +AUTHENTIK_SECRET_KEY=notsosecretkey + +# Authentik Database +AUTHENTIK_DB_NAME=authentik +AUTHENTIK_DB_USER=authentik +AUTHENTIK_DB_PASSWORD=password + +# OIDC Client Configuration +# These must match the provider configurations in the blueprint +OIDC_AUTHENTIK_CLIENT_NAME=oasis-server +OIDC_AUTHENTIK_CLIENT_SECRET=EfNMUM3GG1bd1CYUvNfiBGWKfvbGFiNAdutEqHSarZ9H7oL0sZfKLvPT1ujaqVm2839Vka8Ky0elliMQ6yWKN8Jv8dzh3BeVFn0F7LPquGkIus6JJ9nGH1vtfCt7AhtO + +# Default Users Configuration +# Users are defined in: oidc/authentik/users.yaml +# Edit that file to add/modify users + +# ============================================================================ +# ADVANCED CONFIGURATION +# ============================================================================ + +# Portfolio upload validation (0=off, 1=on) +OASIS_PORTFOLIO_UPLOAD_VALIDATION=0 + +# Worker library versions (empty = latest) +OASIS_OASISLMF_VERSION= +OASIS_ODS_VERSION= +OASIS_ODM_VERSION= +OASIS_OED_SCHEMA_INFO= diff --git a/.env.keycloak b/.env.keycloak new file mode 100644 index 0000000..dabc5fb --- /dev/null +++ b/.env.keycloak @@ -0,0 +1,105 @@ +# ============================================================================ +# KEYCLOAK OIDC AUTHENTICATION CONFIGURATION +# ============================================================================ +# Uses Keycloak as the identity provider for OIDC authentication +# Keycloak Admin: http://localhost:8080/auth/admin (keycloak / password) + +# Project Configuration +COMPOSE_PROJECT_NAME=oasispythonui +OASIS_DEBUG=1 + +# Docker socket path (Docker Desktop uses ~/.docker/desktop/docker.sock) +# Standard Docker: /var/run/docker.sock +DOCKER_SOCK=/var/run/docker.sock + +# Hostname Configuration +OASIS_UI_HOSTNAME=ui.oasis.local +OASIS_PROTOCOL=http + +# Authentication Type +API_AUTH_TYPE=keycloak +OASIS_SERVER_ALLOWED_OIDC_AUTH_PROVIDERS=keycloak,authentik + +# Image Versions +SERVER_IMG=coreoasis/api_server +VERS_API=2.5 +WORKER_IMG=coreoasis/model_worker +VERS_WORKER=2.5 +PYTHONUI_IMG=coreoasis/oasispythonui_app +VERS_UI=latest +VERS_PIWIND=stable/2.5.x + +# Database Configuration +OASIS_SERVER_DB_HOST=server-db +OASIS_SERVER_DB_PORT=5432 +OASIS_SERVER_DB_NAME=oasis +OASIS_SERVER_DB_USER=oasis +OASIS_SERVER_DB_PASS=oasis + +OASIS_CELERY_DB_HOST=celery-db +OASIS_CELERY_DB_PORT=5432 +OASIS_CELERY_DB_NAME=celery +OASIS_CELERY_DB_USER=celery +OASIS_CELERY_DB_PASS=password + +# Broker & Channel Layer +RABBITMQ_DEFAULT_USER=rabbit +RABBITMQ_DEFAULT_PASS=rabbit +OASIS_CELERY_BROKER_URL=amqp://rabbit:rabbit@broker:5672 +REDIS_HOST=channel-layer +REDIS_PORT=6379 +OASIS_SERVER_CHANNEL_LAYER_SSL=false + +# ============================================================================ +# KEYCLOAK CONFIGURATION +# ============================================================================ + +# Keycloak Service +KEYCLOAK_HOST=keycloak +KEYCLOAK_PORT=8080 + +# Keycloak Admin Console Credentials +KEYCLOAK_ADMIN_USER=keycloak +KEYCLOAK_ADMIN_PASSWORD=password + +# Keycloak Database +KEYCLOAK_DB_NAME=keycloak +KEYCLOAK_DB_USER=keycloak +KEYCLOAK_DB_PASSWORD=password + +# OIDC Client Configuration +# These match the realm configuration in oidc/keycloak/oasis-realm.json.template +OIDC_KEYCLOAK_CLIENT_NAME=oasis-server +OIDC_KEYCLOAK_CLIENT_SECRET=e4f4fb25-2250-4210-a7d6-9b16c3d2ab77 + +# Service Account Client (for service-to-service auth) +OASIS_SERVICE_CLIENT_NAME=oasis-service +OASIS_SERVICE_CLIENT_SECRET=serviceNotSoSecret + +# Advanced Configuration +OASIS_PORTFOLIO_UPLOAD_VALIDATION=0 +OASIS_OASISLMF_VERSION= +OASIS_ODS_VERSION= +OASIS_ODM_VERSION= +OASIS_OED_SCHEMA_INFO= + +# ============================================================================ +# User Configuration: +# - Default users defined in: oidc/keycloak/users.yaml +# - Edit that file to add/modify users +# - Users: admin (admin), user (non-admin) +# +# Quick Start: +# 1. cp .env.keycloak .env +# 2. ./install.sh +# 3. Wait for Keycloak to start (can take 2-3 minutes first time) +# 4. Access Keycloak Admin: http://localhost:8080/auth/admin +# 5. Access UI: http://localhost:8501 +# 6. Login via Keycloak +# +# OIDC Endpoints (routed through traefik on port 80): +# - Authorization: http://localhost/auth/realms/oasis/protocol/openid-connect/auth +# - Token: http://localhost/auth/realms/oasis/protocol/openid-connect/token +# - UserInfo: http://localhost/auth/realms/oasis/protocol/openid-connect/userinfo +# - Keycloak Admin (direct): http://localhost:8080/auth/admin +# ============================================================================ diff --git a/.env.simple b/.env.simple new file mode 100644 index 0000000..50d4b45 --- /dev/null +++ b/.env.simple @@ -0,0 +1,70 @@ +# ============================================================================ +# SIMPLE AUTHENTICATION CONFIGURATION +# ============================================================================ +# This is the simplest setup - no external identity provider needed +# Login credentials: admin / password + +# Project Configuration +COMPOSE_PROJECT_NAME=oasispythonui +OASIS_DEBUG=1 + +# Docker socket path (Docker Desktop uses ~/.docker/desktop/docker.sock) +# Standard Docker: /var/run/docker.sock +DOCKER_SOCK=/var/run/docker.sock + +# Hostname Configuration +OASIS_UI_HOSTNAME=ui.oasis.local +OASIS_PROTOCOL=http + +# Authentication Type +API_AUTH_TYPE=simple +OASIS_SERVER_ALLOWED_OIDC_AUTH_PROVIDERS=keycloak,authentik + +# Simple Auth Credentials +OASIS_ADMIN_USER=admin +OASIS_ADMIN_PASS=password + +# Image Versions +SERVER_IMG=coreoasis/api_server +VERS_API=2.5 +WORKER_IMG=coreoasis/model_worker +VERS_WORKER=2.5 +PYTHONUI_IMG=coreoasis/oasispythonui_app +VERS_UI=latest +VERS_PIWIND=stable/2.5.x + +# Database Configuration +OASIS_SERVER_DB_HOST=server-db +OASIS_SERVER_DB_PORT=5432 +OASIS_SERVER_DB_NAME=oasis +OASIS_SERVER_DB_USER=oasis +OASIS_SERVER_DB_PASS=oasis + +OASIS_CELERY_DB_HOST=celery-db +OASIS_CELERY_DB_PORT=5432 +OASIS_CELERY_DB_NAME=celery +OASIS_CELERY_DB_USER=celery +OASIS_CELERY_DB_PASS=password + +# Broker & Channel Layer +RABBITMQ_DEFAULT_USER=rabbit +RABBITMQ_DEFAULT_PASS=rabbit +OASIS_CELERY_BROKER_URL=amqp://rabbit:rabbit@broker:5672 +REDIS_HOST=channel-layer +REDIS_PORT=6379 +OASIS_SERVER_CHANNEL_LAYER_SSL=false + +# Advanced Configuration +OASIS_PORTFOLIO_UPLOAD_VALIDATION=0 +OASIS_OASISLMF_VERSION= +OASIS_ODS_VERSION= +OASIS_ODM_VERSION= +OASIS_OED_SCHEMA_INFO= + +# ============================================================================ +# Quick Start: +# 1. cp .env.simple .env +# 2. ./install.sh +# 3. Access UI at: http://localhost:8501 +# 4. Login with: admin / password +# ============================================================================ diff --git a/.gitignore b/.gitignore index 570b440..85566a2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,13 @@ tmp/ # Act files .actrc -*.env -*.env + +# Environment-specific files (keep examples) +.env +!.env.example +!.env.simple +!.env.keycloak +!.env.authentik # Byte-compiled / optimized / DLL files __pycache__/ @@ -175,3 +180,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Generated OIDC configurations +oidc/*/generated/ diff --git a/.streamlit/config.toml b/.streamlit/config.toml index 8f73e2e..fe84af0 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -6,6 +6,8 @@ toolbarMode = "minimal" [server] fileWatcherType = "none" headless = true +enableXsrfProtection = false +enableCORS = false [browser] gatherUsageStats = false diff --git a/.streamlit/secrets.toml b/.streamlit/secrets.toml index f1ca843..e356f22 100644 --- a/.streamlit/secrets.toml +++ b/.streamlit/secrets.toml @@ -1,2 +1,7 @@ +auth_type='oidc' + user='admin' -password='catmodels' +password='password' + +client_id='oasis-service' +client_secret='serviceNotSoSecret' diff --git a/README.md b/README.md index 572f86e..aa06eb1 100644 --- a/README.md +++ b/README.md @@ -11,29 +11,173 @@ The current version of the UI contains the following pages: - `/dashboard` - View the output of completed analyses. - `/simplified` - Simplified UI which allows for the running of analyses using previously loaded portfolios & models. -## Installation +## Prerequisites -We include a demo docker installation to run the UI. The pre-requisites for -this installation is `git`, `docker` and `docker-compose`. To run the -deployment clone this repo and run the `./install.sh` script. +- `git` +- `docker` with Compose v2 (`docker compose`) -This installation is based on the -[OasisEvaluation](https://github.com/OasisLMF/OasisEvaluation) repo. It will -initialise the [Oasis Platform](https://github.com/OasisLMF/OasisPlatform) with -the test [PiWind](https://github.com/OasisLMF/OasisPiWind) model. +## Quick Start -The UI can then be accessed at -[http://localhost:8501/](http://localhost:8501/). A single default user will be -initialised with the following credentials: +### 1. Choose an authentication type +Three modes are supported. Copy the matching environment template: + +```bash +cp .env.simple .env # No OIDC — username/password login +cp .env.keycloak .env # Keycloak OIDC +cp .env.authentik .env # Authentik OIDC +``` + +Edit `.env` to adjust the hostname, passwords, or image versions if needed. + +### 2. Configure `.streamlit/secrets.toml` + +This file tells the UI how to authenticate against the Oasis API backend. +Edit `.streamlit/secrets.toml` before running the installer — it is mounted +read-only into the UI container. + +**Simple auth:** +```toml +auth_type = 'simple' +user = 'admin' +password = 'password' +``` + +**Keycloak or Authentik OIDC:** +```toml +auth_type = 'oidc' +client_id = 'oasis-service' +client_secret = 'serviceNotSoSecret' +``` + +### 3. Add the hostname to `/etc/hosts` + +The default hostname is `ui.oasis.local`. Add it to your hosts file so your +browser can resolve it: + +```bash +echo "127.0.0.1 ui.oasis.local" | sudo tee -a /etc/hosts +``` + +### 4. Run the installer + +```bash +./install.sh +``` + +The installer clones the PiWind demo model, processes OIDC templates (if +applicable), builds the UI image, and starts all services. It will prompt +before redeploying if a previous installation is detected. + +To tear everything down (removes containers and volumes): + +```bash +./install.sh --uninstall +``` + +## Access Points + +All services are reachable on port 80 via Traefik after a successful install: + +| Service | URL | +|---------|-----| +| UI | `http://ui.oasis.local/` | +| API | `http://ui.oasis.local/api/` | +| Keycloak Admin | `http://ui.oasis.local/auth/` | +| Authentik Admin | `http://ui.oasis.local/authentik/` | + +## Switching Between Auth Types + +1. Bring down the current stack: + ```bash + ./install --uninstall + ``` +2. Copy the new `.env` template and edit if needed: + ```bash + cp .env.keycloak .env + ``` +3. Update `.streamlit/secrets.toml` to match if required (see step 2 of Quick Start). +4. Re-run the installer: + ```bash + ./install.sh + ``` + +## Docker Compose Architecture + +The stack is assembled from multiple Compose files depending on auth type: + +``` +Always loaded: + docker-compose.yml # Core platform: server, worker, databases, broker + docker-compose.ui.yml # Streamlit UI + Traefik reverse proxy + +Conditionally loaded: + docker-compose.keycloak.yml # Keycloak + its PostgreSQL DB (API_AUTH_TYPE=keycloak) + docker-compose.authentik.yml # Authentik + its PostgreSQL DB (API_AUTH_TYPE=authentik) ``` -Username: admin -Password: password + +`install.sh` builds the correct `docker compose -f ... up` command automatically. + +## Key Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `API_AUTH_TYPE` | Auth mode: `simple`, `keycloak`, or `authentik` | `authentik` | +| `OASIS_UI_HOSTNAME` | Hostname the UI and proxy listen on | `ui.oasis.local` | +| `OASIS_PROTOCOL` | `http` or `https` | `http` | +| `VERS_API` | Oasis server image tag | `2.5` | +| `VERS_WORKER` | Oasis worker image tag | `2.5` | +| `VERS_UI` | Python UI image tag | `latest` | + +See the `.env.*` templates for the full list with inline comments. + +## Adding Users + +### Simple auth + +The default admin user (`admin` / `password`) is created automatically. +Additional users must be added via the Oasis API or admin interface. + +### Keycloak + +Edit `oidc/keycloak/users.yaml` and re-run `./install.sh`, or add users +through the Keycloak admin console at `/auth/` (`keycloak` / `password`). + +### Authentik + +Edit `oidc/authentik/users.yaml` and re-run `./install.sh`, or add users +through the Authentik admin console at `/authentik/` (`akadmin` / `password`). + +## Troubleshooting + +Usually first thing to try before anything is clearing browser cache/cookies for the hostname. + +**OIDC login redirects to the wrong URL** +- Confirm `OASIS_UI_HOSTNAME` in `.env` matches the hostname you use in the browser. +- Confirm the same hostname resolves locally (check `/etc/hosts`). + +**UI cannot reach the API** +- Verify Traefik is running: `docker compose ps traefik`. +- Check that the server container is healthy: `docker compose ps server`. +- Inspect Traefik routing logs: `docker compose logs traefik`. + +**Keycloak / Authentik container unhealthy** +- Check logs: `docker compose logs keycloak` or `docker compose logs authentik-server`. +- The IdP database container must be healthy first: `docker compose ps`. +- First startup can take 2–3 minutes while blueprints and realms are imported. + +**Logs and status** +```bash +docker compose ps # service health +docker compose logs -f # UI logs ``` -Note that if previous Oasis docker installations are present they will be -removed during this installation. +## Security Notes + +- The `.env` templates and `users.yaml` files ship with **demo credentials**. + Change all passwords before any non-local deployment. +## Public Demo The public site is at https://ui.oasislmf-scenarios.com/ -Default scenarios in the tool are processed/hosted at https://github.com/OasisLMF/Scenarios +Default scenarios in the tool are processed/hosted at https://github.com/OasisLMF/Scenarios diff --git a/app.py b/app.py index 5e7437f..0cb042b 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,7 @@ from modules.config import retrieve_ui_config -from modules.authorisation import handle_login +from modules.authorisation import ( + get_auth_type, exchange_session_token +) import streamlit as st from modules.client import ClientInterface from modules.nav import SidebarNav @@ -10,28 +12,56 @@ logger = logging.getLogger(__name__) st.set_page_config( - page_title = "OasisLMF", - layout="centered" + page_title="OasisLMF", + layout="centered" ) SidebarNav() ui_config = retrieve_ui_config() +auth_type = get_auth_type() + +# Handle OIDC callback: server redirects here with ?session_token=... after IdP login +if auth_type != 'simple' and "client_interface" not in st.session_state: + session_token = st.query_params.get("session_token") + if session_token: + with st.spinner("Authenticating..."): + try: + token_data = exchange_session_token(session_token) + access_token = token_data['access_token'] + refresh_token = token_data.get('refresh_token') or '' + client_interface = ClientInterface(access_token=access_token, refresh_token=refresh_token) + st.session_state["client_interface"] = client_interface + st.session_state["client"] = client_interface.client + id_token = token_data.get('id_token') + if id_token: + st.session_state["id_token"] = id_token + st.query_params.clear() + st.rerun() + except Exception as e: + st.query_params.clear() + logger.error(f"OIDC login failed: {e}") + st.error(f"Login failed: {e}") if ui_config.skip_login: with st.spinner("Loading platform..."): - client_interface = ClientInterface(username=st.secrets["user"], password=st.secrets["password"]) - st.session_state["client"] = client_interface.client + if auth_type != 'simple': + client_interface = ClientInterface(auth_type="oidc", client_id=st.secrets["client_id"], client_secret=st.secrets["client_secret"]) + else: + client_interface = ClientInterface(auth_type="simple", username=st.secrets["user"], password=st.secrets["password"]) + st.session_state["client"] = client_interface.client cols = st.columns([0.1, 0.8, 0.1]) with cols[1]: st.image(image="images/oasis_logo.png") if "client" in st.session_state: - st.write("Logged in") post_login_page = ui_config.post_login_page if post_login_page: st.switch_page(post_login_page) +elif auth_type != 'simple': + st.html('
' + '
') else: with st.form("login_form"): user = st.text_input("Username", key="username") diff --git a/docker-compose.authentik.yml b/docker-compose.authentik.yml new file mode 100644 index 0000000..edc03c8 --- /dev/null +++ b/docker-compose.authentik.yml @@ -0,0 +1,109 @@ +volumes: + authentik-db-data: + +services: + model-registration: + depends_on: + authentik-server: + condition: service_healthy + authentik-worker: + condition: service_healthy + + authentik-db: + restart: always + image: postgres:15-alpine + environment: + POSTGRES_DB: ${AUTHENTIK_DB_NAME} + POSTGRES_USER: ${AUTHENTIK_DB_USER} + POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD} + volumes: + - authentik-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${AUTHENTIK_DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + authentik-server: + restart: always + image: ghcr.io/goauthentik/server:2025.6.4 + command: server + ports: + - "${AUTHENTIK_PORT:-9000}:9000" + labels: + - "traefik.enable=true" + - "traefik.http.routers.authentik.rule=PathPrefix(`/authentik`)" + - "traefik.http.services.authentik.loadbalancer.server.port=9000" + - "traefik.http.routers.authentik.entrypoints=web" + - "traefik.http.routers.authentik-secure.rule=PathPrefix(`/authentik`)" + - "traefik.http.routers.authentik-secure.entrypoints=websecure" + - "traefik.http.routers.authentik-secure.tls=true" + - "traefik.http.routers.authentik-secure.service=authentik" + environment: + # Bootstrap configuration + AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD} + AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL} + AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + + # Database configuration + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME} + AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER} + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + + # Redis configuration (reuses main channel-layer) + AUTHENTIK_REDIS__HOST: channel-layer + AUTHENTIK_REDIS__PORT: 6379 + + # Authentik configuration + AUTHENTIK_LOG_LEVEL: info + AUTHENTIK_WEB__PATH: /authentik/ + AUTHENTIK_DISABLE_UPDATE_CHECK: "true" + depends_on: + authentik-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "ak healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 90s + + authentik-worker: + restart: always + image: ghcr.io/goauthentik/server:2025.6.4 + command: worker + environment: + # Bootstrap configuration + AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD} + AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL} + AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + + # Database configuration + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME} + AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER} + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + + # Redis configuration + AUTHENTIK_REDIS__HOST: channel-layer + AUTHENTIK_REDIS__PORT: 6379 + + # Authentik configuration + AUTHENTIK_LOG_LEVEL: info + AUTHENTIK_DISABLE_UPDATE_CHECK: "true" + volumes: + - ./oidc/authentik/generated/oasis-blueprint.yaml:/blueprints/oasis-blueprint.yaml:ro + depends_on: + authentik-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "ak healthcheck || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 60s diff --git a/docker-compose.keycloak.yml b/docker-compose.keycloak.yml new file mode 100644 index 0000000..d360fcc --- /dev/null +++ b/docker-compose.keycloak.yml @@ -0,0 +1,73 @@ +volumes: + keycloak-db-data: + +services: + model-registration: + depends_on: + keycloak: + condition: service_healthy + + keycloak-db: + restart: always + image: postgres:15-alpine + environment: + POSTGRES_DB: ${KEYCLOAK_DB_NAME} + POSTGRES_USER: ${KEYCLOAK_DB_USER} + POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD} + volumes: + - keycloak-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${KEYCLOAK_DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + keycloak: + restart: always + image: quay.io/keycloak/keycloak:23.0.6-0 + command: ["start", "--import-realm", "--health-enabled=true"] + ports: + - "${KEYCLOAK_PORT:-8080}:8080" + labels: + - "traefik.enable=true" + - "traefik.http.routers.keycloak.rule=PathPrefix(`/auth`)" + - "traefik.http.services.keycloak.loadbalancer.server.port=8080" + environment: + # Database configuration + KC_DB: postgres + KC_DB_URL_HOST: keycloak-db + KC_DB_URL_PORT: 5432 + KC_DB_URL_DATABASE: ${KEYCLOAK_DB_NAME} + KC_DB_USERNAME: ${KEYCLOAK_DB_USER} + KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD} + + # Admin credentials + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + + # Keycloak configuration + KC_HTTP_RELATIVE_PATH: /auth + # KC_HOSTNAME: ${OASIS_UI_HOSTNAME:-localhost} + KC_HOSTNAME_STRICT: "false" + KC_PROXY: edge + KC_PROXY_ADDRESS_FORWARDING: "true" + KC_LOG_LEVEL: INFO + PROXY_ADDRESS_FORWARDING: "true" + + # Import configuration + KC_IMPORT: /opt/keycloak/data/import/oasis-realm.json + volumes: + - ./oidc/keycloak/generated/oasis-realm.json:/opt/keycloak/data/import/oasis-realm.json:ro + depends_on: + keycloak-db: + condition: service_healthy + healthcheck: + test: + - "CMD" + - "bash" + - "-c" + - 'exec 3<>/dev/tcp/localhost/8080; echo -e "GET /auth/health/ready HTTP/1.1\r\nhost: localhost\r\nConnection: close\r\n\r\n" >&3; grep "200" <&3' + interval: 10s + timeout: 5s + retries: 30 + start_period: 60s diff --git a/docker-compose.ui.yml b/docker-compose.ui.yml new file mode 100644 index 0000000..3d11d41 --- /dev/null +++ b/docker-compose.ui.yml @@ -0,0 +1,53 @@ +services: + pythonui: + restart: always + image: ${PYTHONUI_IMG}:${VERS_UI} + build: + dockerfile: ./oasisui_st_app.Dockerfile + ports: + - "8501:8501" + labels: + - "traefik.enable=true" + # HTTP router (normal UI access) + - "traefik.http.routers.pythonui.rule=PathPrefix(`/`)" + - "traefik.http.routers.pythonui.entrypoints=web" + - "traefik.http.routers.pythonui.priority=1" + # HTTPS router (OIDC callback redirects to /?session_token=... on HTTPS) + - "traefik.http.routers.pythonui-secure.rule=PathPrefix(`/`)" + - "traefik.http.routers.pythonui-secure.entrypoints=websecure" + - "traefik.http.routers.pythonui-secure.tls=true" + - "traefik.http.routers.pythonui-secure.priority=1" + # Shared service + - "traefik.http.services.pythonui.loadbalancer.server.port=8501" + environment: + - API_URL=http://server:8000 + - API_AUTH_TYPE=${API_AUTH_TYPE} + - OASIS_HOSTNAME=${OASIS_UI_HOSTNAME} + - OASIS_PROTOCOL=${OASIS_PROTOCOL:-http} + volumes: + - ./defaults/:/usr/src/app/defaults/ + - ./ui-config.json:/usr/src/app/ui-config.json:ro + - ./.streamlit/:/usr/src/app/.streamlit:ro + healthcheck: + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8501/_stcore/health')"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + depends_on: + server: + condition: service_healthy + develop: + watch: + - action: sync + path: ./app.py + target: /usr/src/app/app.py + - action: sync + path: ./pages/ + target: /usr/src/app/pages/ + - action: sync + path: ./modules/ + target: /usr/src/app/modules/ + - action: sync + path: ./ui-config.json + target: /usr/src/app/ui-config.json diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a72e3cf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,310 @@ +volumes: + server-db-data: + celery-db-data: + filestore-data: + +# ============================================================================ +# Shared Environment Variables +# ============================================================================ + +x-shared-env: &shared-env + # Debug and basic config + OASIS_DEBUG: ${OASIS_DEBUG:-1} + OASIS_URL_SUB_PATH: 0 + + # Hostname configuration (matches Minikube ingress) + INGRESS_EXTERNAL_HOST: ${OASIS_UI_HOSTNAME:-localhost} + INGRESS_INTERNAL_HOST: traefik + + # Authentication configuration + OASIS_SERVER_API_AUTH_TYPE: ${API_AUTH_TYPE:-simple} + OASIS_SERVER_ALLOWED_OIDC_AUTH_PROVIDERS: ${OASIS_SERVER_ALLOWED_OIDC_AUTH_PROVIDERS} + + # Database connections + OASIS_SERVER_DB_ENGINE: django.db.backends.postgresql + OASIS_SERVER_DB_HOST: ${OASIS_SERVER_DB_HOST} + OASIS_SERVER_DB_PORT: ${OASIS_SERVER_DB_PORT} + OASIS_SERVER_DB_NAME: ${OASIS_SERVER_DB_NAME} + OASIS_SERVER_DB_USER: ${OASIS_SERVER_DB_USER} + OASIS_SERVER_DB_PASS: ${OASIS_SERVER_DB_PASS} + + OASIS_CELERY_DB_ENGINE: db+postgresql+psycopg + OASIS_CELERY_DB_HOST: ${OASIS_CELERY_DB_HOST} + OASIS_CELERY_DB_PORT: ${OASIS_CELERY_DB_PORT} + OASIS_CELERY_DB_NAME: ${OASIS_CELERY_DB_NAME} + OASIS_CELERY_DB_USER: ${OASIS_CELERY_DB_USER} + OASIS_CELERY_DB_PASS: ${OASIS_CELERY_DB_PASS} + + # Broker and channel layer + OASIS_CELERY_BROKER_URL: ${OASIS_CELERY_BROKER_URL} + OASIS_SERVER_CHANNEL_LAYER_HOST: ${REDIS_HOST} + OASIS_SERVER_CHANNEL_LAYER_PORT: ${REDIS_PORT:-6379} + OASIS_SERVER_CHANNEL_LAYER_SSL: "false" + + # Task queues + OASIS_TASK_CONTROLLER_QUEUE: task-controller + OASIS_INPUT_GENERATION_CONTROLLER_QUEUE: task-controller + OASIS_LOSSES_GENERATION_CONTROLLER_QUEUE: task-controller + + # Service account credentials (set dynamically by install.sh) + OASIS_SERVICE_USERNAME_OR_ID: ${OASIS_SERVICE_USERNAME_OR_ID} + OASIS_SERVICE_PASSWORD_OR_SECRET: ${OASIS_SERVICE_PASSWORD_OR_SECRET} + OASIS_USE_OIDC: ${OASIS_USE_OIDC} + + # OIDC configuration (only used when API_AUTH_TYPE != simple) + OASIS_SERVER_OIDC_ENDPOINT: ${OASIS_SERVER_OIDC_ENDPOINT:-} + OASIS_SERVER_OIDC_CLIENT_NAME: ${OASIS_SERVER_OIDC_CLIENT_NAME:-} + OASIS_SERVER_OIDC_CLIENT_SECRET: ${OASIS_SERVER_OIDC_CLIENT_SECRET:-} + OASIS_SERVER_OIDC_SERVICE_CLIENT_NAME: ${OASIS_SERVER_OIDC_SERVICE_CLIENT_NAME:-} + OASIS_SERVER_OIDC_SERVICE_CLIENT_SECRET: ${OASIS_SERVER_OIDC_SERVICE_CLIENT_SECRET:-} + +x-volumes: &shared-volumes + - filestore-data:/shared-fs:rw + +# ============================================================================ +# Services +# ============================================================================ + +services: + traefik: + restart: always + image: traefik:v2.11 + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:8443" + - "--ping=true" + - "--api.insecure=true" + ports: + - "80:80" + - "443:8443" + - "8090:8080" + volumes: + - ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock:ro + healthcheck: + test: ["CMD", "traefik", "healthcheck", "--ping"] + interval: 10s + timeout: 5s + retries: 5 + + server: + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["./wsgi/run-wsgi.sh"] + ports: + - "8000:8000" + - "51970:51970" + labels: + - "traefik.enable=true" + # HTTP router + - "traefik.http.routers.server-api.rule=PathPrefix(`/api`)" + - "traefik.http.routers.server-api.entrypoints=web" + - "traefik.http.routers.server-api.middlewares=strip-api" + # HTTPS router (OIDC callback from IdP uses https redirect_uri) + - "traefik.http.routers.server-api-secure.rule=PathPrefix(`/api`)" + - "traefik.http.routers.server-api-secure.entrypoints=websecure" + - "traefik.http.routers.server-api-secure.tls=true" + - "traefik.http.routers.server-api-secure.middlewares=strip-api" + # Shared middleware and service + - "traefik.http.middlewares.strip-api.stripprefix.prefixes=/api" + - "traefik.http.services.server-api.loadbalancer.server.port=8000" + environment: + <<: *shared-env + STARTUP_RUN_MIGRATIONS: "true" + OASIS_PORTFOLIO_UPLOAD_VALIDATION: ${OASIS_PORTFOLIO_UPLOAD_VALIDATION:-0} + volumes: *shared-volumes + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + channel-layer: + condition: service_started + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:8000/healthcheck/"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 30s + + server-websocket: + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["./asgi/run-asgi.sh"] + ports: + - "8001:8001" + labels: + - "traefik.enable=true" + - "traefik.http.routers.server-ws.rule=PathPrefix(`/ws`)" + - "traefik.http.middlewares.strip-ws.stripprefix.prefixes=/ws" + - "traefik.http.routers.server-ws.middlewares=strip-ws" + - "traefik.http.services.server-ws.loadbalancer.server.port=8001" + environment: + <<: *shared-env + volumes: *shared-volumes + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + channel-layer: + condition: service_started + healthcheck: + test: ["CMD", "python3", "-c", "import socket; s=socket.socket(); s.settimeout(5); s.connect(('localhost', 8001)); s.close()"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + + v2-worker-monitor: + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["celery", "-A", "src.server.oasisapi.celery_app_v2", "worker", "--loglevel=INFO", "-Q", "celery-v2"] + environment: + <<: *shared-env + volumes: *shared-volumes + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + + v2-task-controller: + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["celery", "-A", "src.server.oasisapi.celery_app_v2", "worker", "--loglevel=INFO", "-Q", "task-controller"] + environment: + <<: *shared-env + volumes: *shared-volumes + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + + celery-beat-v2: + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["celery", "-A", "src.server.oasisapi.celery_app_v2", "beat", "--loglevel=INFO"] + environment: + <<: *shared-env + volumes: *shared-volumes + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + + model-registration: + restart: "no" + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["python3", "/scripts/model_registration.py"] + environment: + <<: *shared-env + OASIS_API_URL: "http://server:8000" + OASIS_MODEL_SUPPLIER_ID: OasisLMF + OASIS_MODEL_ID: PiWind + OASIS_MODEL_VERSION_ID: v2 + OASIS_RUN_MODE: v2 + OASIS_MODEL_DATA_DIRECTORY: /model/meta-data + volumes: + - ./OasisPiWind/:/model:ro + - ./scripts:/scripts:ro + depends_on: + server: + condition: service_healthy + + piwind-worker: + restart: always + image: ${WORKER_IMG:-coreoasis/model_worker}:${VERS_WORKER:-latest} + environment: + <<: *shared-env + OASIS_MODEL_SUPPLIER_ID: OasisLMF + OASIS_MODEL_ID: PiWind + OASIS_MODEL_VERSION_ID: v2 + OASIS_RUN_MODE: v2 + OASIS_OASISLMF_VERSION: ${OASIS_OASISLMF_VERSION} + OASIS_ODS_VERSION: ${OASIS_ODS_VERSION} + OASIS_ODM_VERSION: ${OASIS_ODM_VERSION} + OASIS_OED_SCHEMA_INFO: ${OASIS_OED_SCHEMA_INFO} + volumes: + - ./OasisPiWind/:/home/worker/model + - filestore-data:/shared-fs:rw + depends_on: + celery-db: + condition: service_healthy + broker: + condition: service_healthy + model-registration: + condition: service_completed_successfully + + server-db: + restart: always + image: postgres:15-alpine + environment: + POSTGRES_DB: ${OASIS_SERVER_DB_NAME} + POSTGRES_USER: ${OASIS_SERVER_DB_USER} + POSTGRES_PASSWORD: ${OASIS_SERVER_DB_PASS} + volumes: + - server-db-data:/var/lib/postgresql/data:rw + ports: + - "33307:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${OASIS_SERVER_DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + celery-db: + restart: always + image: postgres:15-alpine + environment: + POSTGRES_DB: ${OASIS_CELERY_DB_NAME} + POSTGRES_USER: ${OASIS_CELERY_DB_USER} + POSTGRES_PASSWORD: ${OASIS_CELERY_DB_PASS} + volumes: + - celery-db-data:/var/lib/postgresql/data:rw + ports: + - "33306:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${OASIS_CELERY_DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + broker: + restart: always + image: rabbitmq:3.11-management-alpine + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + ports: + - "5672:5672" + - "15672:15672" + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + channel-layer: + restart: always + image: valkey/valkey:8.1-alpine3.21 + ports: + - "${REDIS_PORT}:6379" + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/install.sh b/install.sh index b47ebb6..bc661bb 100755 --- a/install.sh +++ b/install.sh @@ -1,71 +1,277 @@ #!/bin/bash set -e +# ============================================================================ +# OasisPythonUI Installation Script +# ============================================================================ +# Supports authentication types: simple, keycloak, authentik +# Auto-detects auth type from .env file and includes appropriate services. + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -export $(grep -v '^#' .env | xargs) +# ============================================================================ +# Uninstall mode +# ============================================================================ -export VERS_MDK=latest -export VERS_API=latest -export VERS_WORKER=latest -export VERS_UI=latest -export VERS_PIWIND='stable/2.3.x' +if [[ "$1" == "--uninstall" || "$1" == "-u" ]]; then + echo "Uninstalling Oasis platform (docker compose down only)..." -export SERVER_IMG=coreoasis/api_server -export WORKER_IMG=coreoasis/model_worker -export GIT_PIWIND=OasisPiWind + set +e + docker compose -f "$SCRIPT_DIR/docker-compose.yml" \ + -f "$SCRIPT_DIR/docker-compose.ui.yml" \ + -f "$SCRIPT_DIR/docker-compose.keycloak.yml" \ + -f "$SCRIPT_DIR/docker-compose.authentik.yml" \ + down --remove-orphans -v 2>/dev/null + # Also try old files in case of migration + docker compose -f "$SCRIPT_DIR/oasis-platform.yml" \ + -f "$SCRIPT_DIR/oasis-ui.yml" \ + down --remove-orphans -v 2>/dev/null + set -e -MSG=$(cat <<-END -Do you want to reinstall? -Note: This will wipe uploaded exposure and run data from the local API. -END -) + echo "Uninstall complete." + exit 0 +fi +# ============================================================================ +# Load configuration from .env +# ============================================================================ -# Check for prev install and offer to clean wipe -if [[ $(docker volume ls | grep OasisData -c) -gt 1 || -d $SCRIPT_DIR/$GIT_PIWIND ]]; then - while true; do read -r -n 1 -p "${MSG:-Continue?} [y/n]: " REPLY - case $REPLY in - [yY]) echo ; WIPE=1; break ;; - [nN]) echo ; WIPE=0; break ;; - *) printf " \033[31m %s \n\033[0m" "invalid input" - esac - done +if [ ! -f "$SCRIPT_DIR/.env" ]; then + echo "ERROR: .env file not found!" + echo "" + echo "Create one from the examples:" + echo " cp .env.simple .env # Simple JWT authentication" + echo " cp .env.keycloak .env # Keycloak OIDC" + echo " cp .env.authentik .env # Authentik OIDC" + exit 1 +fi + +set -a +source "$SCRIPT_DIR/.env" +set +a + +# ============================================================================ +# Auto-detect Docker socket if not set +# ============================================================================ + +if [ -z "$DOCKER_SOCK" ]; then + # Default to /var/run/docker.sock - Docker Desktop provides this path + # transparently to containers even if the host socket is elsewhere. + export DOCKER_SOCK=/var/run/docker.sock +elif [ ! -S "$DOCKER_SOCK" ]; then + echo "WARNING: DOCKER_SOCK=$DOCKER_SOCK does not exist or is not a socket" + echo " Falling back to /var/run/docker.sock" + export DOCKER_SOCK=/var/run/docker.sock +fi + +echo "========================================" +echo " OasisPythonUI Installer" +echo "========================================" +echo "" +echo " Auth type: $API_AUTH_TYPE" +echo " Hostname: $OASIS_UI_HOSTNAME" +echo " Protocol: $OASIS_PROTOCOL" +echo " Docker socket: $DOCKER_SOCK" +echo "" + +# ============================================================================ +# Validate configuration +# ============================================================================ + +echo "--- Validating configuration ---" +bash "$SCRIPT_DIR/scripts/validate-config.sh" +echo "" + +# ============================================================================ +# Set service account credentials based on auth type +# ============================================================================ + +echo "--- Configuring service credentials ---" + +if [ "$API_AUTH_TYPE" = "simple" ]; then + export OASIS_SERVICE_USERNAME_OR_ID="$OASIS_ADMIN_USER" + export OASIS_SERVICE_PASSWORD_OR_SECRET="$OASIS_ADMIN_PASS" + export OASIS_USE_OIDC="" + echo " -> Simple auth: using admin credentials for service account" + +elif [ "$API_AUTH_TYPE" = "keycloak" ]; then + export OASIS_SERVER_OIDC_SERVICE_CLIENT_NAME="$OASIS_SERVICE_CLIENT_NAME" + export OASIS_SERVER_OIDC_SERVICE_CLIENT_SECRET="$OASIS_SERVICE_CLIENT_SECRET" + export OASIS_SERVER_OIDC_CLIENT_NAME="$OIDC_KEYCLOAK_CLIENT_NAME" + export OASIS_SERVER_OIDC_CLIENT_SECRET="$OIDC_KEYCLOAK_CLIENT_SECRET" + export OASIS_SERVER_OIDC_ENDPOINT="${OASIS_PROTOCOL}://${OASIS_UI_HOSTNAME}/auth/realms/oasis/protocol/openid-connect/" + export OASIS_SERVICE_USERNAME_OR_ID="$OASIS_SERVICE_CLIENT_NAME" + export OASIS_SERVICE_PASSWORD_OR_SECRET="$OASIS_SERVICE_CLIENT_SECRET" + export OASIS_USE_OIDC="true" + echo " -> Keycloak auth: using OIDC client credentials" + echo " -> OIDC endpoint: $OASIS_SERVER_OIDC_ENDPOINT" + +elif [ "$API_AUTH_TYPE" = "authentik" ]; then + export OASIS_SERVER_OIDC_SERVICE_CLIENT_NAME="$OASIS_SERVICE_CLIENT_NAME" + export OASIS_SERVER_OIDC_SERVICE_CLIENT_SECRET="$OASIS_SERVICE_CLIENT_SECRET" + export OASIS_SERVER_OIDC_CLIENT_NAME="$OIDC_AUTHENTIK_CLIENT_NAME" + export OASIS_SERVER_OIDC_CLIENT_SECRET="$OIDC_AUTHENTIK_CLIENT_SECRET" + export OASIS_SERVER_OIDC_ENDPOINT="${OASIS_PROTOCOL}://${OASIS_UI_HOSTNAME}/authentik/application/o/" + export OASIS_SERVICE_USERNAME_OR_ID="$OASIS_SERVICE_CLIENT_NAME" + export OASIS_SERVICE_PASSWORD_OR_SECRET="$OASIS_SERVICE_CLIENT_SECRET" + export OASIS_USE_OIDC="true" + echo " -> Authentik auth: using OIDC client credentials" + echo " -> OIDC endpoint: $OASIS_SERVER_OIDC_ENDPOINT" +fi + +echo "" + +# ============================================================================ +# Process OIDC templates (if applicable) +# ============================================================================ + +if [ "$API_AUTH_TYPE" = "keycloak" ]; then + echo "--- Processing Keycloak templates ---" + bash "$SCRIPT_DIR/oidc/keycloak/process-keycloak-templates.sh" + echo "" +elif [ "$API_AUTH_TYPE" = "authentik" ]; then + echo "--- Processing Authentik templates ---" + bash "$SCRIPT_DIR/oidc/authentik/process-authentik-templates.sh" + echo "" +fi + +# ============================================================================ +# Build compose file list +# ============================================================================ + +COMPOSE_FILES="-f $SCRIPT_DIR/docker-compose.yml -f $SCRIPT_DIR/docker-compose.ui.yml" + +if [ "$API_AUTH_TYPE" = "keycloak" ]; then + COMPOSE_FILES="$COMPOSE_FILES -f $SCRIPT_DIR/docker-compose.keycloak.yml" + echo " -> Including Keycloak services" +elif [ "$API_AUTH_TYPE" = "authentik" ]; then + COMPOSE_FILES="$COMPOSE_FILES -f $SCRIPT_DIR/docker-compose.authentik.yml" + echo " -> Including Authentik services" +fi + +echo " -> Compose files: $COMPOSE_FILES" +echo "" + +# ============================================================================ +# Clone PiWind model (if not present) +# ============================================================================ + +GIT_PIWIND=OasisPiWind - if [[ "$WIPE" == 1 ]]; then - docker compose -f $SCRIPT_DIR/portainer.yaml down --remove-orphans - - printf "Deleting docker container:\n" - set +e - docker compose -f $SCRIPT_DIR/oasis-platform.yml -f $SCRIPT_DIR/oasis-ui.yml down --remove-orphans - set -e - printf "Deleting docker data: \n" - rm -rf $SCRIPT_DIR/$GIT_PIWIND - docker volume ls | grep OasisData | awk 'BEGIN { FS = "[ \t\n]+" }{ print $2 }' | xargs -r docker volume rm - else - echo "-- Reinstall aborted -- " - exit 1 - fi +if [ ! -d "$SCRIPT_DIR/$GIT_PIWIND/.git" ]; then + echo "--- Cloning PiWind model ---" + mkdir -p "$SCRIPT_DIR/$GIT_PIWIND" + cd "$SCRIPT_DIR/$GIT_PIWIND" + git clone --depth 1 --branch "${VERS_PIWIND}" "https://github.com/OasisLMF/$GIT_PIWIND.git" . + cd "$SCRIPT_DIR" + echo "" +else + echo " -> PiWind model already cloned" + echo "" fi +# ============================================================================ +# Check for previous install +# ============================================================================ -# --- Clone PiWind ---------------------------------------------------------- # +EXISTING_CONTAINERS=$(docker compose $COMPOSE_FILES ps -q 2>/dev/null | wc -l) -mkdir -p $SCRIPT_DIR/$GIT_PIWIND -cd $SCRIPT_DIR/$GIT_PIWIND -git clone --depth 1 --branch $VERS_PIWIND https://github.com/OasisLMF/$GIT_PIWIND.git . -git checkout $VERS_PIWIND +if [ "$EXISTING_CONTAINERS" -gt 0 ]; then + MSG="Previous installation detected. Redeploy? (existing data will be preserved)" + while true; do + read -r -n 1 -p "${MSG} [y/n]: " REPLY + case $REPLY in + [yY]) echo ; break ;; + [nN]) echo ; echo "-- Aborted --"; exit 1 ;; + *) printf " \033[31m %s \n\033[0m" "invalid input" ;; + esac + done +fi -# --- RUN Oasis Platform & UI ----------------------------------------------- # +# ============================================================================ +# Pull images +# ============================================================================ -cd $SCRIPT_DIR +echo "--- Pulling images ---" set +e -docker pull ${WORKER_IMG:-coreoasis/model_worker}:${VERS_WORKER:-latest} -docker pull ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} -docker pull ${PYTHONUI_IMG-coreoasis/oasispythonui_app}:${VERS_API:-latest} +docker pull "${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest}" +docker pull "${WORKER_IMG:-coreoasis/model_worker}:${VERS_WORKER:-latest}" +docker pull "${PYTHONUI_IMG:-coreoasis/oasispythonui_app}:${VERS_UI:-latest}" set -e -# RUN OasisPlatform / OasisUI / Portainer -docker compose -f $SCRIPT_DIR/oasis-platform.yml up -d --no-build -docker compose -f $SCRIPT_DIR/oasis-ui.yml up -d +echo "" + +# ============================================================================ +# Deploy services +# ============================================================================ + +echo "--- Deploying services ---" + +# Build UI +docker compose $COMPOSE_FILES build --no-cache pythonui + +# Start all services +docker compose $COMPOSE_FILES up -d + +echo "" + +# ============================================================================ +# Wait for health checks +# ============================================================================ + +echo "--- Waiting for services to be healthy ---" + +# Wait for core services +echo " -> Waiting for server..." +docker compose $COMPOSE_FILES exec -T server sh -c 'for i in $(seq 1 60); do curl -sf http://localhost:8000/healthcheck/ > /dev/null 2>&1 && exit 0; sleep 5; done; exit 1' || { + echo " WARNING: Server health check timed out. Check logs with: docker compose $COMPOSE_FILES logs server" +} + +if [ "$API_AUTH_TYPE" = "keycloak" ]; then + echo " -> Waiting for Keycloak (this may take 2-3 minutes on first start)..." + timeout 180 bash -c "while ! docker compose $COMPOSE_FILES ps keycloak 2>/dev/null | grep -q 'healthy'; do sleep 10; done" 2>/dev/null || { + echo " WARNING: Keycloak health check timed out. Check logs with: docker compose $COMPOSE_FILES logs keycloak" + } +elif [ "$API_AUTH_TYPE" = "authentik" ]; then + echo " -> Waiting for Authentik..." + timeout 180 bash -c "while ! docker compose $COMPOSE_FILES ps authentik-server 2>/dev/null | grep -q 'healthy'; do sleep 10; done" 2>/dev/null || { + echo " WARNING: Authentik health check timed out. Check logs with: docker compose $COMPOSE_FILES logs authentik-server" + } +fi + +echo "" + +# ============================================================================ +# Summary +# ============================================================================ + +echo "========================================" +echo " Deployment Complete!" +echo "========================================" +echo "" +echo " Auth type: $API_AUTH_TYPE" +echo " Hostname: $OASIS_UI_HOSTNAME" +echo "" +echo " Access Points (via Traefik on port 80):" +echo " UI: http://${OASIS_UI_HOSTNAME}/" +echo " API: http://${OASIS_UI_HOSTNAME}/api/" +echo " API Docs: http://${OASIS_UI_HOSTNAME}/api/swagger/" + +if [ "$API_AUTH_TYPE" = "keycloak" ]; then + echo " Keycloak: http://${OASIS_UI_HOSTNAME}/auth/admin" + echo " (${KEYCLOAK_ADMIN_USER} / ${KEYCLOAK_ADMIN_PASSWORD})" +elif [ "$API_AUTH_TYPE" = "authentik" ]; then + echo " Authentik: http://${OASIS_UI_HOSTNAME}/authentik/if/admin" + echo " (akadmin / ${AUTHENTIK_BOOTSTRAP_PASSWORD})" +fi + +if [ "$API_AUTH_TYPE" = "simple" ]; then + echo "" + echo " Login: ${OASIS_ADMIN_USER} / ${OASIS_ADMIN_PASS}" +fi + +echo "" +echo " Logs: docker compose $COMPOSE_FILES logs -f" +echo " Stop: docker compose $COMPOSE_FILES down" +echo "" diff --git a/modules/authorisation.py b/modules/authorisation.py index 1c772cc..19d43a6 100644 --- a/modules/authorisation.py +++ b/modules/authorisation.py @@ -1,6 +1,8 @@ ''' Module to handle authorisation. ''' +import os +import requests from requests.exceptions import HTTPError from modules.config import retrieve_ui_config import streamlit as st @@ -9,6 +11,34 @@ logger = logging.getLogger(__name__) + +def get_auth_type(): + return os.environ.get('API_AUTH_TYPE', 'simple') + + +def exchange_session_token(session_token): + """Exchange a session_token (from OIDC callback) for access/refresh tokens. + + The session_token is a short-lived JWT created by the server after + successful OIDC authentication. POST it to the server's session_token + endpoint to retrieve the actual tokens. + """ + api_url = os.environ.get('API_URL', 'http://localhost:8000') + response = requests.post( + f"{api_url}/oidc/session_token/", + json={"session_token": session_token}, + ) + response.raise_for_status() + return response.json() + + +def logout(): + """Clear session state and rerun (simple auth logout).""" + for key in list(st.session_state.keys()): + del st.session_state[key] + st.rerun() + + def handle_login(skip_login=False): """Handle the redirect behaviour for login or initalise if login skipped. @@ -21,7 +51,13 @@ def handle_login(skip_login=False): if skip_login: with st.spinner("Loading platform..."): try: - st.session_state["client_interface"] = ClientInterface(username=st.secrets["user"], password=st.secrets["password"]) + auth_type = get_auth_type() + if auth_type != 'simple': + st.session_state["client_interface"] = ClientInterface( + auth_type="oidc", client_id=st.secrets["client_id"], client_secret=st.secrets["client_secret"]) + else: + st.session_state["client_interface"] = ClientInterface( + auth_type="simple", username=st.secrets["user"], password=st.secrets["password"]) except HTTPError as e: logger.error(e) st.error("Loading platform failed.") @@ -30,17 +66,24 @@ def handle_login(skip_login=False): # Go to login page st.switch_page("app.py") + def quiet_login(): if "client_interface" in st.session_state: return - if "user" in st.secrets and "password" in st.secrets: - try: - st.session_state["client_interface"] = ClientInterface(username=st.secrets["user"], password=st.secrets["password"]) - except HTTPError as e: - logger.error(e) + auth_type = get_auth_type() + try: + if auth_type != 'simple': + if "client_id" in st.secrets and "client_secret" in st.secrets: + st.session_state["client_interface"] = ClientInterface( + auth_type="oidc", client_id=st.secrets["client_id"], client_secret=st.secrets["client_secret"]) + elif "user" in st.secrets and "password" in st.secrets: + st.session_state["client_interface"] = ClientInterface(auth_type="simple", username=st.secrets["user"], password=st.secrets["password"]) + except HTTPError as e: + logger.error(e) return + def validate_page(page_path): ui_config = retrieve_ui_config() diff --git a/modules/client.py b/modules/client.py index 2ac7771..cbbae89 100644 --- a/modules/client.py +++ b/modules/client.py @@ -8,20 +8,24 @@ logger = get_session_logger() + class JsonEndpointInterface: ''' Abstract class for handling a endpoint of the Oasis APIClient restricted to json output. ''' + def __init__(self, client, endpoint_name='portfolios'): self.endpoint = getattr(client, endpoint_name) def get(self, ID=None): return self.endpoint.get(ID=ID).json() + class SettingsTemplateInterface: ''' Class for handling interactions with the SettingsTemplateEndpoint of the Oasis APIClient ''' + def __init__(self, client): self.endpoint = getattr(client, 'setting_templates', None) self.content = getattr(self.endpoint, 'content', None) @@ -35,11 +39,13 @@ def get(self, model_pk, ID=None): def get_contents(self, model_pk, ID): return self.content.get(model_pk, ID).json() + class EndpointInterface: ''' Abstract class for handling a endpoint of the Oasis APIClient. Includes handling both file and json endpoints. ''' + def __init__(self, client, endpoint_name='portfolios'): self.endpoint = getattr(client, endpoint_name) @@ -55,7 +61,7 @@ def search(self, metadata={}): def get_file(self, ID, filename, df=False): file_available = self.get(ID).get(filename, None) if file_available is None: - logger.error(f'File not available. Analysis ID: {ID }Filename: {filename}') + logger.error(f'File not available. Analysis ID: {ID}Filename: {filename}') return None data = getattr(self.endpoint, filename) @@ -70,6 +76,7 @@ class ModelsEndpointInterface(EndpointInterface): ''' Interface for models endpoint of the Oasis APIClient. ''' + def __init__(self, client): super().__init__(client, endpoint_name='models') self.settings = JsonEndpointInterface(self.endpoint, endpoint_name='settings') @@ -80,6 +87,7 @@ class AnalysesEndpointInterface(EndpointInterface): ''' Interface for analyses endpoint of the Oasis APIClient. ''' + def __init__(self, client): super().__init__(client, endpoint_name='analyses') self.settings = JsonEndpointInterface(self.endpoint, endpoint_name='settings') @@ -112,6 +120,7 @@ class PortfoliosEndpointInterface(EndpointInterface): ''' Interface for portfolios endpoint of the Oasis APIClient. ''' + def __init__(self, client): super().__init__(client, "portfolios") self.client = client @@ -128,8 +137,8 @@ def get_reinsurance_info_file(self, ID, df=False): def get_reinsurance_scope_file(self, ID, df=False): return self.get_file(ID, "reinsurance_scope_file", df) - def create(self, name, location_file = None, accounts_file = None, - reinsurance_info_file = None, reinsurance_scope_file = None): + def create(self, name, location_file=None, accounts_file=None, + reinsurance_info_file=None, reinsurance_scope_file=None): ''' Create a portfolio using the `UploadedFile` objects created when using the `st.file_uploader`. @@ -151,14 +160,13 @@ def prepare_upload_f(fname, fbytes): location_f = prepare_upload_f('location_file', location_file) accounts_f = prepare_upload_f('accounts_file', accounts_file) ri_info_f = prepare_upload_f('reinsurance_info_file', reinsurance_info_file) - ri_scope_f= prepare_upload_f('reinsurance_scope_file', reinsurance_scope_file) - - self.client.upload_inputs(portfolio_name = name, - location_fp = location_f, - accounts_fp = accounts_f, - ri_info_fp = ri_info_f, - ri_scope_fp = ri_scope_f) + ri_scope_f = prepare_upload_f('reinsurance_scope_file', reinsurance_scope_file) + self.client.upload_inputs(portfolio_name=name, + location_fp=location_f, + accounts_fp=accounts_f, + ri_info_fp=ri_info_f, + ri_scope_fp=ri_scope_f) class ClientInterface: @@ -171,11 +179,16 @@ class ClientInterface: analyses: Interface for managing analyses. models: Interface for managing models. ''' - def __init__(self, client=None, username=None, password=None): + + def __init__(self, client=None, auth_type=None, username=None, password=None, access_token=None, refresh_token=None, client_id=None, client_secret=None): api_url = os.environ.get('API_URL', 'http://localhost:8000') - if username is not None and password is not None: - client = APIClient(username=username, password=password, api_url=api_url) + if auth_type == "token" or (access_token is not None and refresh_token is not None): + client = APIClient(api_url=api_url, auth_type="token", access_token=access_token, refresh_token=refresh_token) + elif auth_type == "oidc": + client = APIClient(api_url=api_url, auth_type="oidc", client_id=client_id, client_secret=client_secret) + elif username is not None and password is not None: + client = APIClient(api_url=api_url, auth_type="simple", username=username, password=password) assert client is not None, 'Client not set' @@ -184,11 +197,10 @@ def __init__(self, client=None, username=None, password=None): self.analyses = AnalysesEndpointInterface(client) self.models = ModelsEndpointInterface(client) - def create_analysis(self, portfolio_id, model_id, analysis_name): - resp = self.client.create_analysis(portfolio_id = portfolio_id, - model_id = model_id, - analysis_name = analysis_name) + resp = self.client.create_analysis(portfolio_id=portfolio_id, + model_id=model_id, + analysis_name=analysis_name) return resp diff --git a/modules/nav.py b/modules/nav.py index acc86a5..86f3b45 100644 --- a/modules/nav.py +++ b/modules/nav.py @@ -1,17 +1,27 @@ from modules.config import retrieve_ui_config +from modules.authorisation import logout, get_auth_type import streamlit as st ui_config = retrieve_ui_config() + def SidebarNav(no_client=False): + with st.sidebar: if "client_interface" in st.session_state or no_client: for page_config in ui_config.pages: st.page_link(page_config['path'], label=page_config['label']) + if "client_interface" in st.session_state: + if get_auth_type() != 'simple': + id_token = st.session_state.get("id_token", "") + st.html(f'
' + f'' + '
') + else: + if st.button("Logout", use_container_width=True): + logout() else: st.page_link('app.py', label="Login") # Add logo st.logo(image="images/oasis_logo_bg.png", size="large") - - diff --git a/oasis-platform.yml b/oasis-platform.yml index edaea86..e5334d3 100755 --- a/oasis-platform.yml +++ b/oasis-platform.yml @@ -1,6 +1,8 @@ volumes: server-db-OasisData: celery-db-OasisData: + keycloak-db-OasisData: + authentik-db-OasisData: filestore-OasisData: x-shared-env: &shared-env OASIS_DEBUG: 1 @@ -26,78 +28,97 @@ x-volumes: &shared-volumes - filestore-OasisData:/shared-fs:rw services: server: - restart: always - image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} - command: ["./wsgi/run-wsgi.sh"] - ports: - - 8000:8000 - - 51970:51970 - links: - - server-db - - celery-db - - broker - environment: - <<: *shared-env - STARTUP_RUN_MIGRATIONS: "true" - OASIS_ADMIN_USER: admin - OASIS_ADMIN_PASS: catmodels - volumes: - - filestore-OasisData:/shared-fs:rw - healthcheck: + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["./wsgi/run-wsgi.sh"] + ports: + - 8000:8000 + - 51970:51970 + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + keycloak: + condition: service_healthy + environment: + <<: *shared-env + STARTUP_RUN_MIGRATIONS: "true" + OASIS_ADMIN_USER: admin + OASIS_ADMIN_PASS: catmodels + volumes: + - filestore-OasisData:/shared-fs:rw + healthcheck: test: curl --fail http:localhost:8000/healthcheck/ || exit interval: 30s retries : 10 start_period: 30s timeout: 10s server_websocket: - restart: always - image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} - command: ["./asgi/run-asgi.sh"] - links: - - server-db - - celery-db - - broker - ports: - - 8001:8001 - environment: - <<: *shared-env - volumes: - - filestore-OasisData:/shared-fs:rw + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: ["./asgi/run-asgi.sh"] + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + keycloak: + condition: service_healthy + ports: + - 8001:8001 + environment: + <<: *shared-env + volumes: + - filestore-OasisData:/shared-fs:rw v2-worker-monitor: - restart: always - image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} - command: [celery, -A, 'src.server.oasisapi.celery_app_v2', worker, --loglevel=INFO, -Q, celery-v2] - links: - - server-db - - celery-db - - broker - environment: - <<: *shared-env - volumes: - - filestore-OasisData:/shared-fs:rw + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: [celery, -A, 'src.server.oasisapi.celery_app_v2', worker, --loglevel=INFO, -Q, celery-v2] + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + environment: + <<: *shared-env + volumes: + - filestore-OasisData:/shared-fs:rw v2-task-controller: - restart: always - image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} - command: [celery, -A, 'src.server.oasisapi.celery_app_v2', worker, --loglevel=INFO, -Q, task-controller] - links: - - server-db - - celery-db - - broker - environment: - <<: *shared-env - volumes: - - filestore-OasisData:/shared-fs:rw + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: [celery, -A, 'src.server.oasisapi.celery_app_v2', worker, --loglevel=INFO, -Q, task-controller] + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + environment: + <<: *shared-env + volumes: + - filestore-OasisData:/shared-fs:rw celery-beat_v2: - restart: always - image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} - command: [celery, -A, src.server.oasisapi.celery_app_v2, beat, --loglevel=INFO] - links: - - server-db - - celery-db - - broker - environment: - <<: *shared-env - volumes: *shared-volumes + restart: always + image: ${SERVER_IMG:-coreoasis/api_server}:${VERS_API:-latest} + command: [celery, -A, src.server.oasisapi.celery_app_v2, beat, --loglevel=INFO] + depends_on: + server-db: + condition: service_healthy + celery-db: + condition: service_healthy + broker: + condition: service_healthy + environment: + <<: *shared-env + volumes: *shared-volumes piwind-worker: restart: always image: ${WORKER_IMG:-coreoasis/model_worker}:${VERS_WORKER:-latest} @@ -105,17 +126,17 @@ services: context: . dockerfile: Dockerfile.model_worker links: - - celery-db - - broker:mybroker + - celery-db + - broker:mybroker environment: - <<: *shared-env - OASIS_MODEL_SUPPLIER_ID: OasisLMF - OASIS_MODEL_ID: PiWind - OASIS_MODEL_VERSION_ID: 'v2' - OASIS_RUN_MODE: v2 + <<: *shared-env + OASIS_MODEL_SUPPLIER_ID: OasisLMF + OASIS_MODEL_ID: PiWind + OASIS_MODEL_VERSION_ID: 'v2' + OASIS_RUN_MODE: v2 volumes: - - ./OasisPiWind/:/home/worker/model - - filestore-OasisData:/shared-fs:rw + - ./OasisPiWind/:/home/worker/model + - filestore-OasisData:/shared-fs:rw server-db: restart: always image: postgres @@ -126,7 +147,12 @@ services: volumes: - server-db-OasisData:/var/lib/postgresql:rw ports: - - 33307:3306 + - 33307:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U oasis -d oasis"] + interval: 5s + timeout: 5s + retries: 5 celery-db: restart: always image: postgres @@ -138,6 +164,11 @@ services: - celery-db-OasisData:/var/lib/postgresql:rw ports: - 33306:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U celery -d celery"] + interval: 5s + timeout: 5s + retries: 5 broker: restart: always image: rabbitmq:3.8.14-management @@ -147,8 +178,93 @@ services: ports: - 5672:5672 - 15672:15672 + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "check_port_connectivity"] + interval: 5s + timeout: 5s + retries: 5 channel-layer: restart: always image: redis:5.0.7 ports: - 6379:6379 + keycloak-db: + restart: always + image: postgres + environment: + - POSTGRES_DB=keycloak + - POSTGRES_USER=keycloak + - POSTGRES_PASSWORD=password + volumes: + - keycloak-db-OasisData:/var/lib/postgresql:rw + ports: + - 33308:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"] + interval: 5s + timeout: 5s + retries: 5 + authentik-db: + restart: always + image: postgres + environment: + - POSTGRES_DB=authentik + - POSTGRES_USER=authentik + - POSTGRES_PASSWORD=password + volumes: + - authentik-db-OasisData:/var/lib/postgresql:rw + ports: + - 33309:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U authentik -d authentik"] + interval: 5s + timeout: 5s + retries: 5 + keycloak-realm-render: + image: alpine:3.19 + volumes: + - ./oidc/keycloak:/scripts + working_dir: /scripts + command: sh -c "apk add --no-cache bash perl yq util-linux && bash gen-users.sh" + keycloak: + image: quay.io/keycloak/keycloak:23.0.6-0 + restart: always + command: + - start + - --import-realm + depends_on: + keycloak-db: + condition: service_healthy + keycloak-realm-render: + condition: service_completed_successfully + ports: + - 8080:8080 + environment: + # Admin user + KEYCLOAK_ADMIN: keycloak + KEYCLOAK_ADMIN_PASSWORD: password + # Logging + KC_LOGLEVEL: DEBUG + # HTTP / proxy behavior + KC_HTTP_RELATIVE_PATH: /auth + KC_PROXY: edge + KC_PROXY_ADDRESS_FORWARDING: "true" + KC_HOSTNAME_STRICT: "false" + # Realm import + KC_IMPORT: /opt/keycloak/data/import/oasis-realm.json + # Database config + KC_DB: postgres + KC_DB_URL_HOST: keycloak-db + KC_DB_URL_PORT: 5432 + KC_DB_URL_DATABASE: keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: password + volumes: + - ./oidc/keycloak/rendered/oasis-realm.json:/opt/keycloak/data/import/oasis-realm.json:ro + healthcheck: + test: + - "CMD-SHELL" + - 'exec 3<>/dev/tcp/localhost/8080; echo -e "GET /auth/realms/master HTTP/1.1\r\nhost: localhost\r\nConnection: close\r\n\r\n" >&3; grep "HTTP/1.1 200 OK" <&3' + interval: 10s + timeout: 5s + retries: 30 diff --git a/oidc/README.md b/oidc/README.md new file mode 100644 index 0000000..f9ed232 --- /dev/null +++ b/oidc/README.md @@ -0,0 +1,34 @@ +# Update default oasis OIDC configuration +## Keycloak Realms +1. Open a shell on the keycloak pod: + + ``` + kubectl exec -it deployment/keycloak bash + ``` + +2. Do the export, this will actually bring up the server, but once it is up we can shut it down: + + ``` + # Start and wait for: ....Admin console listening on http://127.0.0.1:10090 + /opt/jboss/keycloak/bin/standalone.sh \ + -Djboss.socket.binding.port-offset=100 -Dkeycloak.migration.action=export \ + -Dkeycloak.migration.provider=singleFile \ + -Dkeycloak.migration.realmName=oasis \ + -Dkeycloak.migration.usersExportStrategy=REALM_FILE \ + -Dkeycloak.migration.file=/tmp/oasis-realm.json + ``` + +3. Download file from pod: + + ``` + # Get the pod name: + PN=$(kubectl get pods -l app=keycloak --no-headers -o custom-columns=":metadata.name") + + # Download the export: + kubectl cp $PN:/tmp/oasis-realm.json oasis-realm.json + ``` + +# Authentik Blueprints +Authentik does have the functionality to export blueprints, however these are often exported into a large, unordered yaml file containing all default authentik configuration data, and everything is linked together by random primary keys, making this file essentially not human readable and difficult to cut down and edit. + +The best way to modify Authentiks configuration is to directly edit the `blueprints/oasis-blueprint.yaml` file, using the [default github blueprints](https://github.com/goauthentik/authentik/tree/main/blueprints) as an example. \ No newline at end of file diff --git a/oidc/authentik/README.md b/oidc/authentik/README.md new file mode 100644 index 0000000..a8f055d --- /dev/null +++ b/oidc/authentik/README.md @@ -0,0 +1,50 @@ +# Authentik Configuration for OasisPythonUI + +This directory contains Authentik-specific configuration files for OIDC authentication. + +## Files + +- **users.yaml** - User definitions (edit to add/modify users) +- **oasis-blueprint.yaml.template** - Authentik blueprint template with ___VAR___ placeholders +- **oasis-users-blueprint.yaml.template** - User entry template +- **process-authentik-templates.sh** - Template processing script +- **generated/** - Runtime-generated configurations (gitignored) + +## How It Works + +1. **At deployment time**, `process-authentik-templates.sh` is executed +2. Script reads `users.yaml` and environment variables +3. For each user, generates a user entry from `oasis-users-blueprint.yaml.template` +4. Combines all users and injects into `oasis-blueprint.yaml.template` +5. Replaces hostname variables with actual values +6. Outputs final blueprint configuration to `generated/oasis-blueprint.yaml` +7. Authentik worker imports the blueprint on startup + +## Adding Users + +Edit `users.yaml`: + +```yaml +users: + - username: analyst + password: analyst123 + admin: false +``` + +Then redeploy: + +```bash +./install.sh +``` + +## Template Variables + +Variables replaced by the processing script: + +- `___USERNAME___` - username from users.yaml +- `___PASSWORD___` - password from users.yaml +- `___GROUPS___` - YAML array of groups based on admin flag +- `___USERS___` - YAML array of all generated user entries +- `___OASIS_PROTOCOL___` - http or https from .env +- `___OASIS_UI_HOSTNAME___` - hostname from .env +- `___REDIRECT_BASE___` - \${OASIS_PROTOCOL}://\${OASIS_UI_HOSTNAME} diff --git a/oidc/authentik/oasis-blueprint.yaml.template b/oidc/authentik/oasis-blueprint.yaml.template new file mode 100644 index 0000000..3cf7ca4 --- /dev/null +++ b/oidc/authentik/oasis-blueprint.yaml.template @@ -0,0 +1,155 @@ +version: 1 +metadata: + name: custom-oasis-providers + labels: + system: "false" + +# See here for examples: https://github.com/goauthentik/authentik/tree/main/blueprints + +entries: +- model: authentik_core.group + state: created + identifiers: + name: admin + attrs: + is_superuser: true + id: oasis_admin_group +- model: authentik_providers_oauth2.oauth2provider + state: present + identifiers: + name: provider-for-swagger + attrs: + name: "Provider for swagger" + client_id: swagger + client_secret: ZEbHaO5irVER9MJRu48PSglwHTbk4fHTkRSrdABYGpvkWlIgj1uReEXXkhBOnLHV5TwzuM5ASqFH4fHv6c9bDNYNnQqp3a5QT7niJSs5ulfu1ASFdYZb5s16m4UlHcPE + client_type: confidential + include_claims_in_id_token: true + access_code_validity: minutes=1 + access_token_validity: minutes=5 + refresh_token_validity: days=30 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + issuer_mode: global + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + sub_mode: hashed_user_id + redirect_uris: + - matching_mode: regex + url: ".*" + - matching_mode: strict + url: "___REDIRECT_BASE___" + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]] + +- model: authentik_core.application + state: present + identifiers: + slug: swagger + attrs: + name: swagger + slug: swagger + policy_engine_mode: any + provider: !Find [authentik_providers_oauth2.oauth2provider, [name, provider-for-swagger]] + +- model: authentik_providers_oauth2.oauth2provider + state: present + identifiers: + name: provider-for-oasis-server + attrs: + name: "Provider for oasis-server" + client_id: oasis-server + client_secret: EfNMUM3GG1bd1CYUvNfiBGWKfvbGFiNAdutEqHSarZ9H7oL0sZfKLvPT1ujaqVm2839Vka8Ky0elliMQ6yWKN8Jv8dzh3BeVFn0F7LPquGkIus6JJ9nGH1vtfCt7AhtO + client_type: confidential + include_claims_in_id_token: true + access_code_validity: minutes=1 + access_token_validity: minutes=5 + refresh_token_validity: days=30 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + issuer_mode: global + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + sub_mode: hashed_user_id + redirect_uris: + - matching_mode: regex + url: ".*" + - matching_mode: strict + url: "___REDIRECT_BASE___" + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]] + +- model: authentik_core.application + state: present + identifiers: + slug: oasis-server + attrs: + name: oasis-server + slug: oasis-server + policy_engine_mode: any + provider: !Find [authentik_providers_oauth2.oauth2provider, [name, provider-for-oasis-server]] + + +- model: authentik_providers_oauth2.scopemapping + state: present + identifiers: + managed: goauthentik.io/providers/oauth2/scope-profile-service-account + attrs: + name: "Scope: profile with is_service_account" + scope_name: profile + description: "General profile information with is_service_account" + expression: | + return { + # This is the exact same as profile but has an extra field is_service_account + "name": request.user.name, + "given_name": request.user.name, + "preferred_username": request.user.username, + "nickname": request.user.username, + "groups": [group.name for group in request.user.ak_groups.all()], + "is_service_account": True, + } + +- model: authentik_providers_oauth2.oauth2provider + state: present + identifiers: + name: provider-for-oasis-service + attrs: + name: "Provider for oasis-service" + client_id: oasis-service + client_secret: serviceNotSoSecret + client_type: confidential + include_claims_in_id_token: true + access_code_validity: minutes=1 + access_token_validity: minutes=5 + refresh_token_validity: days=30 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + issuer_mode: global + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + sub_mode: hashed_user_id + redirect_uris: + - matching_mode: regex + url: ".*" + - matching_mode: strict + url: "___REDIRECT_BASE___" + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile-service-account]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]] + +- model: authentik_core.application + state: present + identifiers: + slug: oasis-service + attrs: + name: oasis-service + slug: oasis-service + policy_engine_mode: any + provider: !Find [authentik_providers_oauth2.oauth2provider, [name, provider-for-oasis-service]] + +# Users are added here +___USERS___ diff --git a/oidc/authentik/oasis-users-blueprint.yaml.template b/oidc/authentik/oasis-users-blueprint.yaml.template new file mode 100644 index 0000000..5c0de94 --- /dev/null +++ b/oidc/authentik/oasis-users-blueprint.yaml.template @@ -0,0 +1,12 @@ +- model: authentik_core.user + state: present + identifiers: + username: ___USERNAME___ + attrs: + username: ___USERNAME___ + name: ___USERNAME___ + email: "___USERNAME___@example.com" + is_active: true + password: ___PASSWORD___ + groups: +___GROUPS___ diff --git a/oidc/authentik/process-authentik-templates.sh b/oidc/authentik/process-authentik-templates.sh new file mode 100755 index 0000000..c636250 --- /dev/null +++ b/oidc/authentik/process-authentik-templates.sh @@ -0,0 +1,141 @@ +#!/bin/bash +set -e + +# ============================================================================ +# Authentik Template Processor +# ============================================================================ +# Processes Authentik blueprint and user templates with ___VAR___ placeholders +# Mimics Helm template processing from OasisPlatform Kubernetes deployment + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TEMPLATE_DIR="$SCRIPT_DIR" +OUTPUT_DIR="$SCRIPT_DIR/generated" +USERS_FILE="$SCRIPT_DIR/users.yaml" + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# ============================================================================ +# Parse Users YAML +# ============================================================================ + +echo " -> Parsing users from users.yaml" + +# Convert YAML to JSON for processing +# Try yq first, fallback to python +if command -v yq &> /dev/null; then + USERS_DATA=$(yq eval '.users' "$USERS_FILE" -o=json) +else + USERS_DATA=$(python3 -c " +import yaml, json +with open('$USERS_FILE') as f: + data = yaml.safe_load(f) + print(json.dumps(data['users'])) +") +fi + +if ! command -v jq &> /dev/null; then + echo " ERROR: jq is required but not installed" + exit 1 +fi + +USER_COUNT=$(echo "$USERS_DATA" | jq '. | length') +echo " -> Found $USER_COUNT user(s)" + +if [ "$USER_COUNT" -eq 0 ]; then + echo " ERROR: No users defined in users.yaml" + exit 1 +fi + +# ============================================================================ +# Generate User Blueprint Entries +# ============================================================================ + +echo " -> Generating user entries from template" + +USER_TEMPLATE=$(cat "$TEMPLATE_DIR/oasis-users-blueprint.yaml.template") +USER_ENTRIES=() + +# Process each user +while IFS= read -r user; do + username=$(echo "$user" | jq -r '.username') + password=$(echo "$user" | jq -r '.password') + is_admin=$(echo "$user" | jq -r '.admin') + + echo " - Processing user: $username" + + # Build groups (with proper YAML indentation) + groups=" - !Find [authentik_core.group, [name, authentik Read-only]]" + + if [ "$is_admin" = "true" ]; then + groups="${groups}"$'\n'" - !Find [authentik_core.group, [name, admin]]" + fi + + # Replace variables in template + user_entry="$USER_TEMPLATE" + user_entry="${user_entry//___USERNAME___/$username}" + user_entry="${user_entry//___PASSWORD___/$password}" + user_entry="${user_entry//___GROUPS___/$groups}" + + USER_ENTRIES+=("$user_entry") + +done < <(echo "$USERS_DATA" | jq -c '.[]') + +# Join user entries with newlines +USERS_YAML=$(printf '%s\n' "${USER_ENTRIES[@]}") + +echo " -> Generated ${#USER_ENTRIES[@]} user entry(ies)" + +# ============================================================================ +# Process Main Blueprint Template +# ============================================================================ + +echo " -> Processing blueprint template" + +BLUEPRINT_TEMPLATE=$(cat "$TEMPLATE_DIR/oasis-blueprint.yaml.template") + +# Replace ___USERS___ placeholder +BLUEPRINT_YAML="${BLUEPRINT_TEMPLATE//___USERS___/$USERS_YAML}" + +# Build redirect base URL +REDIRECT_BASE="${OASIS_PROTOCOL}://${OASIS_UI_HOSTNAME}" +echo " -> Using redirect base: $REDIRECT_BASE" + +# Replace hostname placeholders +BLUEPRINT_YAML="${BLUEPRINT_YAML//___OASIS_PROTOCOL___/$OASIS_PROTOCOL}" +BLUEPRINT_YAML="${BLUEPRINT_YAML//___OASIS_UI_HOSTNAME___/$OASIS_UI_HOSTNAME}" +BLUEPRINT_YAML="${BLUEPRINT_YAML//___REDIRECT_BASE___/$REDIRECT_BASE}" + +# ============================================================================ +# Write Output +# ============================================================================ + +OUTPUT_FILE="$OUTPUT_DIR/oasis-blueprint.yaml" +echo "$BLUEPRINT_YAML" > "$OUTPUT_FILE" + +echo " -> Blueprint configuration written to: $OUTPUT_FILE" + +# ============================================================================ +# Validate YAML +# ============================================================================ + +echo " -> Validating generated YAML" + +if command -v yq &> /dev/null; then + if ! yq eval '.' "$OUTPUT_FILE" > /dev/null 2>&1; then + echo " ERROR: Generated YAML is invalid!" + echo " -> Check syntax errors in template or processing logic" + exit 1 + fi + echo " OK Generated YAML is valid" +else + # Fallback to python - use yaml.scan() which checks syntax only and + # does not try to construct objects, so custom tags like !Find are ignored + if ! python3 -c "import yaml; list(yaml.scan(open('$OUTPUT_FILE')))" 2>/dev/null; then + echo " ERROR: Generated YAML is invalid!" + exit 1 + fi + echo " OK Generated YAML is valid" +fi + +echo " OK Authentik template processing complete!" diff --git a/oidc/authentik/users.yaml b/oidc/authentik/users.yaml new file mode 100644 index 0000000..65e0d6f --- /dev/null +++ b/oidc/authentik/users.yaml @@ -0,0 +1,17 @@ +# Authentik User Definitions +# These users will be created via Authentik blueprint during initialization +# Matches OasisPlatform Kubernetes values.yaml: authentik.oasisRestApi.users + +users: + - username: admin + password: password + admin: true + + - username: user + password: password + admin: false + + # Uncomment to add more users: + # - username: analyst + # password: password + # admin: false diff --git a/oidc/keycloak/README.md b/oidc/keycloak/README.md new file mode 100644 index 0000000..e32e087 --- /dev/null +++ b/oidc/keycloak/README.md @@ -0,0 +1,52 @@ +# Keycloak Configuration for OasisPythonUI + +This directory contains Keycloak-specific configuration files for OIDC authentication. + +## Files + +- **users.yaml** - User definitions (edit to add/modify users) +- **oasis-realm.json.template** - Keycloak realm template with ___VAR___ placeholders +- **oasis-realm-user.json.template** - User entry template +- **process-keycloak-templates.sh** - Template processing script +- **generated/** - Runtime-generated configurations (gitignored) + +## How It Works + +1. **At deployment time**, `process-keycloak-templates.sh` is executed +2. Script reads `users.yaml` and environment variables +3. For each user, generates a user entry from `oasis-realm-user.json.template` +4. Combines all users and injects into `oasis-realm.json.template` +5. Replaces hostname variables with actual values +6. Outputs final realm configuration to `generated/oasis-realm.json` +7. Keycloak imports the generated realm on startup + +## Adding Users + +Edit `users.yaml`: + +```yaml +users: + - username: analyst + password: analyst123 + admin: false +``` + +Then redeploy: + +```bash +./install.sh +``` + +## Template Variables + +Variables replaced by the processing script: + +- `___USERNAME___` - username from users.yaml +- `___PASSWORD___` - password from users.yaml +- `___UUID___` - generated UUID +- `___ROLES___` - "default-roles-oasis" +- `___GROUPS___` - "admin" if user.admin=true, else empty +- `___USERS___` - array of all generated user entries +- `___OASIS_PROTOCOL___` - http or https from .env +- `___OASIS_UI_HOSTNAME___` - hostname from .env +- `___REDIRECT_BASE___` - \${OASIS_PROTOCOL}://\${OASIS_UI_HOSTNAME} diff --git a/oidc/keycloak/oasis-realm-user.json.template b/oidc/keycloak/oasis-realm-user.json.template new file mode 100644 index 0000000..b135ce4 --- /dev/null +++ b/oidc/keycloak/oasis-realm-user.json.template @@ -0,0 +1,17 @@ +{ + "id" : "___UUID___", + "createdTimestamp" : 1632406965622, + "username" : "___USERNAME___", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "type" : "password", + "value": "___PASSWORD___" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ ___ROLES___ ], + "notBefore" : 0, + "groups" : [ ___GROUPS___ ] +} diff --git a/oidc/keycloak/oasis-realm.json.template b/oidc/keycloak/oasis-realm.json.template new file mode 100644 index 0000000..706bb6c --- /dev/null +++ b/oidc/keycloak/oasis-realm.json.template @@ -0,0 +1,1976 @@ +{ + "id" : "oasis", + "realm" : "oasis", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "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" : "0604f797-e2bf-46c2-b629-029cfa190b60", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "oasis", + "attributes" : { } + }, { + "id" : "39cd0ba6-8972-4f6f-88b7-ecb0309a44e5", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "oasis", + "attributes" : { } + }, { + "id" : "c5bf9ce4-70d4-444b-a5fe-799d73f43d97", + "name" : "default-roles-oasis", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "oasis", + "attributes" : { } + }, { + "id" : "d2ac062a-950c-4718-9810-c32ba845c657", + "name" : "admin", + "description" : "Oasis superuser", + "composite" : false, + "clientRole" : false, + "containerId" : "oasis", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "b1ca116f-fd4e-44e6-8534-203ab0897374", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "c784f095-edec-40ae-a689-ce1e70be7e63", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "05d92cb5-84b8-4ab6-a79e-144da88c07b0", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "01c6f249-c019-4861-bbeb-7a84978cbe34", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "4882d194-cb14-4a89-853d-ea56cd8ac943", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "814ec555-c829-41da-8bcb-7209886bdcb5", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "1f124a4c-b11d-4ba1-9dd7-807a9afa7723", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "2155fa4a-f43c-4c0e-983e-1a29d45c91b4", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "f2b54e5a-c080-455d-913b-5793c66fe8e5", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "ba81f4eb-ecc1-4c00-9cab-d3a72586f4d3", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "9c90a796-7154-40d6-9252-cf6e957d3c59", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "c8212d20-9d94-45bd-a462-0d4c0b71786d", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "e09951e5-afaf-4eba-973e-ba0ab01b019b", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "66f83c97-eed6-4aaf-b139-5eb133c593e0", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "c1c74262-307a-47c8-b505-f0d1c1ca928d", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "bf000846-edc0-4237-aa7a-fff38fdc4822", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-events", "view-users", "query-groups", "view-identity-providers", "query-clients", "manage-events", "query-realms", "manage-realm", "view-authorization", "manage-identity-providers", "impersonation", "view-realm", "manage-users", "create-client", "manage-clients", "manage-authorization", "query-users", "view-clients" ] + } + }, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "197a39e4-6cb5-40d1-898a-a6aacaf26a87", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "96d8345d-09e8-42f1-afaf-b621438e8a2d", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + }, { + "id" : "15cc2654-ba4b-4a00-b609-feba547519e3", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "oasis-server" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "2740af01-d51c-4f06-8740-55f7adb22110", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "aacabf2d-4e3a-44a5-bf09-689ec9ee4182", + "attributes" : { } + } ], + "account" : [ { + "id" : "4c1e0100-e8e8-49d0-b865-3a17cf2797df", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + }, { + "id" : "040f599a-7951-47fc-8a7f-ba00a3689c26", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + }, { + "id" : "20991db4-8665-41e6-872b-2b37db53c0ae", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + }, { + "id" : "330d6c4d-5fd5-494e-8973-b913a35b34de", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + }, { + "id" : "06d9e0f2-e73e-4eb8-9386-61f57ac94465", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + }, { + "id" : "6e2dc232-b0d4-427c-b702-692d309e4812", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + }, { + "id" : "afc6e2e5-c827-4ae9-a90d-c9fedc32c865", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + }, { + "id" : "534dba00-ba18-4a0b-8a3c-567efc8db9b2", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "attributes" : { } + } ], + "swagger" : [ ] + } + }, + "groups" : [ { + "id" : "a2fc257d-a66b-456f-9527-1961bea79aa0", + "name" : "admin", + "path" : "/admin", + "attributes" : { }, + "realmRoles" : [ "admin" ], + "clientRoles" : { }, + "subGroups" : [ ] + } ], + "defaultRole" : { + "id" : "c5bf9ce4-70d4-444b-a5fe-799d73f43d97", + "name" : "default-roles-oasis", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "oasis" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ "___USERS___" ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "e6de9417-6796-473a-85ad-97d76bebb44a", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/oasis/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/oasis/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "openid", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "e1ed3e81-7e28-454a-baf6-2c6333af8271", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/oasis/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/oasis/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "6f712150-d8f8-446b-9bc7-52377bc27468", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "openid", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "12ebc78e-f41e-4f5d-ab7f-793a612ea5c4", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-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" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "openid", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "aacabf2d-4e3a-44a5-bf09-689ec9ee4182", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-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" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "b9b8c572-956e-4e0e-8113-0f3a97e88c4c", + "clientId" : "oasis-server", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "e4f4fb25-2250-4210-a7d6-9b16c3d2ab77", + "redirectUris" : [ "*" ], + "webOrigins" : [ "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "client.secret.creation.time" : "1677167475", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "use.refresh.tokens" : "true", + "tls-client-certificate-bound-access-tokens" : "false", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "acr.loa.map" : "{}", + "require.pushed.authorization.requests" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "openid", "roles", "profile", "email", "microprofile-jwt" ], + "optionalClientScopes" : [ "address", "phone", "offline_access" ] + }, { + "id" : "5be255ea-e55a-40dc-a6dc-fe286c55979c", + "clientId" : "oasis-service", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "serviceNotSoSecret", + "redirectUris" : [ "*" ], + "webOrigins" : [ "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "client.secret.creation.time" : "1677167475", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "use.refresh.tokens" : "true", + "tls-client-certificate-bound-access-tokens" : "false", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "acr.loa.map" : "{}", + "require.pushed.authorization.requests" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false" + }, + "protocolMappers": [ + { + "name": "Is Service Account", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "is_service_account", + "claim.value": "true", + "jsonType.label": "boolean", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ], + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "openid", "roles", "profile", "email", "microprofile-jwt" ], + "optionalClientScopes" : [ "address", "phone", "offline_access" ] + }, { + "id" : "40223c47-11d9-49e5-a552-197fbbfb21c3", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-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" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "91851ff2-7dee-493c-819d-8c1c30fd6a18", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/oasis/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/oasis/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "bfcd61e9-3d3e-4806-b152-9a2986fa9b1c", + "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", "openid", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "121778e5-3d21-4041-9bb4-dd3872dc764f", + "clientId" : "swagger", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "e4f4fb25-2250-4210-a7d6-9b16c3d2ab77", + "redirectUris" : [ "*" ], + "webOrigins" : [ "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.assertion.signature" : "false", + "id.token.as.detached.signature" : "false", + "saml.multivalued.roles" : "false", + "saml.force.post.binding" : "false", + "saml.encrypt" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "exclude.session.state.from.auth.response" : "false", + "oidc.ciba.grant.enabled" : "false", + "saml.artifact.binding" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "saml_force_name_id_format" : "false", + "saml.client.signature" : "false", + "tls.client.certificate.bound.access.tokens" : "false", + "require.pushed.authorization.requests" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "saml.onetimeuse.condition" : "false" + }, + "authenticationFlowBindingOverrides" : { + "browser" : "17a35dea-0549-4fbb-8766-c04b0ef5ead2" + }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "edc7a8e6-9386-441d-b433-0b6cc38701fa", + "name" : "Groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-group-membership-mapper", + "consentRequired" : false, + "config" : { + "full.path" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "userinfo.token.claim" : "true" + } + } ], + "defaultClientScopes" : [ "web-origins", "openid", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "86531b34-4038-4fb6-a460-af7305028e38", + "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" : "707ed254-580f-4122-90f1-f4cfb93d473e", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "ea2504b5-a04c-4f97-ba21-d83c8887ea3e", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String" + } + }, { + "id" : "83c29cf1-c126-4924-88a9-a2fbf272949e", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "5dd4483e-dc8e-4e55-9f51-064ca274f4b2", + "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" : "a1420ecf-f732-4c83-a01c-202b37a23918", + "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" : "b29a5d93-44fc-47f2-aa1d-e0ed76859641", + "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" : "df929c90-4aa1-454a-a7f5-cc942a4dc8e4", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "7f71b984-52e5-48ff-a580-76160624959a", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "e63ba84c-74aa-4640-a685-c0a6597e8062", + "name" : "openid", + "description" : "test", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "930cb3bb-56d1-4ee5-8c32-7c824e069c21", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "c81b941f-fb24-4327-bf3f-de0c2243898a", + "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" : "669b5885-ba7c-40d2-a6fb-b12b8a3462ca", + "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" : "c875706e-345a-4d4c-96cd-f4ba0cec1c5e", + "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" : "95ef3673-a38d-42c5-ae93-997a05f0e6a3", + "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" : "3fd73c1e-ba89-456d-a5db-868b2222da1a", + "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" : "d8a4b19d-96b6-40a5-b0ca-c9740be6af5a", + "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" : "bb5be793-50ec-4f48-9e1e-747e518d10ef", + "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" : "18dbc673-1ae5-41cd-8631-b7aa738f6e03", + "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" : "7b83a8f0-4f30-4768-846f-44e7b6bcd769", + "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" : "1fee71ff-cf41-43ff-a13f-d8183a1b77b3", + "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" : "b7805536-c836-4aed-be37-e6f9be20cb11", + "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" : "4a7ccb2a-d8bf-4125-8ee5-e4bd45be5902", + "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" : "f99a1ebc-afd3-49e8-bf63-422e626814dd", + "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" : "be73a6f2-a464-4d75-b464-cff825472d69", + "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" : "a2a69cc9-8837-4e35-9f38-ec3583f28980", + "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" : "8c0d01fd-436f-4274-a6a5-a921846f20aa", + "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" : "b6ba966f-02f1-45d4-8d87-a4b9ded36807", + "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" : "3e856c35-c127-4801-af65-02d69042751f", + "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" : "06552fd2-2360-4e28-9293-e05feb44b4bf", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "8c593200-627b-44f4-b6b5-1b69025b381f", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "1100e00b-3185-426d-b73e-1f716d53f4ca", + "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" : "7ce718c5-4cc5-4030-8770-3237224315c4", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "7ee4c29c-a754-4ac1-bc04-413a7e4234ce", + "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" : "edfc3de0-3220-460a-a9f9-961af95b17d5", + "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" : "bad1cf6d-d3eb-434c-8eee-836b57e268e7", + "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" : "21ae480f-ef05-4436-a9d8-a6089272010a", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-group-membership-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "d7759ff1-b0aa-40a3-ab8d-6e8f1cf9b9e7", + "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" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr", "openid" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "045ca875-ebb7-4434-946e-0f41ec4a2997", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "aad248b3-56e6-4c1d-9ed8-37ade618bdac", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "fb31e901-a356-4bfd-b71e-95d1fd6d65a5", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "3f3a39dd-a2f7-45ed-9cbf-920e2aec061b", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "ca50bd0e-6b75-42ac-a5e5-10071349e21c", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "968faeef-74e0-408d-8ca3-e36cfaf24241", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "5c1d8674-1b1f-4723-9aa0-2784339267ad", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "7b319c13-57f6-4fd5-abd6-db3ded8f296d", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "eb1cbf1c-eea2-4c0b-a2b3-7f21ac967f13", + "name" : "rsa-enc-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEogIBAAKCAQEAuX/kizvJwi17AF12VWme2YvwbVlIJeA5IN0rBDp6V5LogM61kIiL/sy9kUzyQHdjTe7mCEkPF27swV9LOrggRsueH0tjmuaFj2ElFdcQwpF7f5Lusb7qPhd41Qb5Zb2H4MOypTTXI6mIudIyY1i2QGJPJL3/a60mfgOAumAhBZoe4YcrxcxOtYSdHAvPX8whP735HUreWNcA5N3vO4mR666Fsu8JQFgoDg+JNMhU08KZin1qFKB3OaAZlgjUgSWrVdRLB3v7mXeXZZATT+wAbwsagGxKgy7/3HvkmG2kEib9UANPvb84vSak417abmFOs+eCjeTFlNZQzdSIZ2EB4wIDAQABAoIBAAniSqcwlFuIWNl4IWoDqrckwbbR3wpFnEWoAzbCdupbpY/Xc80zuWHmPuOTGgVdVyk75X3jhq5U9sCG91c1oGKlTtOUDMagKdWt82/qebYkUlRF2/oUlh6g2YtgR8GoC6lqRAaRfjUkP2jHTnAEthFQK69lgEqYe0iTnzRPEvNdX2/OCZEuTe1hsck3GQDvd1ZB8UyYBjtYYnxBUG0P4DW/SB0kAmwMoG6g00uGcPSbWrPzjyhLQ6jDzzC84VYoFulgXLtX3cZxpwcnfxC2ORLsvociKqYQ6yaFAjOVP+sYFtAJQCpiubg9AxyYVQpK+K8SV6iWU8ae1BkYDGbzNgECgYEA/kzqFJmrb3CsOpUyrAgbM2v+0h8a33SpZczJTbYPVhFwoylRweB7C1vjQXBODXN6R9bzMhcHdEWiw4IqPAX+z23Yp7CPyDkV26fDMvsFbPKh9Txjh0RwclV/qqbISLpWrF1ZSl9XdRVhiGiRpGphZmFhjbDS8761jwi8EPIvc6MCgYEAur1EI0CqLLUvPtTJI+01v15bMHvkIdVTAa6HPQ78VwFHt/yOWwRxKfJM+3+7V94PexlsFITl2CM9jzg571npMy9LPsd7zqAOo1fcQWEfVYcgm76O78z/kDUovwhECYDd9XKDTFwkBImotuZ2SqSx+vCm5IU+qLaPZyn1wSKHHMECgYBbFsvmMyEwWsimd1jePE0Z/z4Yn+GtVwlymIcm2ebmanrRRvStIK6SZAikIQkkUk/jucAFGjCmWmcx5scgFvmt7WfksR7flmsY8h++fCH2Y3bV9BqmkkJBAhUn2HP3cR+owAtC06HtI4p7JRG+NgjLdmhOK590hcRdBsDuxQFwRwKBgFqc5ohYo3roPFG3vRZyz2bZ6VPgejW0pv+k6bjGIcoyM9PieE1QHX1mNta/B3A+r7JjBp/6UPGNQBzUAsDTFyagJ1oCQGBmKFQ4mQcckrDUzgzk7cUM62HVeb0gzKrz3kBw3ada+ps9FSITOIlF25tR1RoEUgBZ/cHoiXi7QWbBAoGAIpdJ+6orP8rkYbEgfbZVBSPhxvJkhGbtuAQLty/ws3C0ErbRTANmwkIX/s3sGSA8FQdmqEMr/eTSYePWwPElXNxDLycJrl5plUz76P8qbu4cNVQiXFzSwOGeFnj4IXnz2pteLMoFYn4ukOTHNTYzMaCI9MLfjYLV9AYoexATLCw=" ], + "certificate" : [ "MIICmTCCAYECBgGGbweeRjANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVvYXNpczAeFw0yMzAyMjAxMzMxNTVaFw0zMzAyMjAxMzMzMzVaMBAxDjAMBgNVBAMMBW9hc2lzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuX/kizvJwi17AF12VWme2YvwbVlIJeA5IN0rBDp6V5LogM61kIiL/sy9kUzyQHdjTe7mCEkPF27swV9LOrggRsueH0tjmuaFj2ElFdcQwpF7f5Lusb7qPhd41Qb5Zb2H4MOypTTXI6mIudIyY1i2QGJPJL3/a60mfgOAumAhBZoe4YcrxcxOtYSdHAvPX8whP735HUreWNcA5N3vO4mR666Fsu8JQFgoDg+JNMhU08KZin1qFKB3OaAZlgjUgSWrVdRLB3v7mXeXZZATT+wAbwsagGxKgy7/3HvkmG2kEib9UANPvb84vSak417abmFOs+eCjeTFlNZQzdSIZ2EB4wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAed7GBSxJV3rTx9SkUjbcJk2smk7h8HufzmAgdguD/VdcjrdNksrh8ytPt5XuqozH3X17XoAYS/zFVpRq4tYSRxPWz8DdVAtZdTrhbt7Oo6re2K3nZcj/l/SvA8pm47mjEWyKh0ywb9ogfXMxiZGYoBLTu42Q9rMDFqVqkj42+WLQlEiv3QCqdm+mAnpChtgHtgIpRlwxoYpf5Zhu1oSolN9L3xVoi6E4Prxt9p9FvyJ3UJbIHQU7tKVLLZcTHVokOXfkI2IftarBjjbKQIsqiIPNXHdR4lRbJyRU+VqVCRz7Pw0Rw0agFdcNOjFDSAkEfNCfF2MPthLPCs5DI9ppX" ], + "priority" : [ "100" ] + } + }, { + "id" : "334422da-b93b-47c9-bbd7-2feb3d96d912", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "8446fe96-b8ce-4f7a-949a-42160897a64e" ], + "secret" : [ "LZx3medrsxwjDq_kIp28yw" ], + "priority" : [ "100" ] + } + }, { + "id" : "75116461-1178-465e-a3ae-576fa85e536e", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAuOFeqG2UdUaubpYhfM+BtrlXeO3SY1SGz/JfoT3ZImte2GfgGCEW9/PINjk7sWgd3tmD/M29XBu9CssMaETwj9XaTtc6mJwuAV8Weo5gOPwRQvIe+CcjFOgIlN8UWHt2oNisCCfmb07uHaprYc48MuriMRLvPpElaeYwDkhUGCEfKwbrHj/CnRXBY+IHA0uuxCQKLiYG5xx3yMV0wkEgNC6K4Wv3ZpUKM9SCjc5mDxX44imLErYpKs0+c08tp8Zif+fB3jVtQxOAg5+GMp0ANbQqZSBtVpmXp6mQEJBjBtyJ7oEOaEV7GgjkSF+D8zA47BVenhK6PRWryWj03kQ02wIDAQABAoIBAASfKwTziv+M3yAdRARhEka6xr4GK/lBtrEMx76PKOtxsF2hPc/nRodiTRdV7PEYFORqSaC7P/Lv2pT3fdLpePNk5DU5BNpQeodG6kKcETm7dhmsqyuG8Zs8hbe7BUDaV/KDbXbdZdGohnlatkkr+lic2JS40B70n/Elc6ltE3Ua693Q0bffyxPyCmDb+KQskCCSGGbGE0wRIfMRZl2plysAMBh+inJmQD3VE7iQRxOuE8WxnH4fImzXT5i2xFoF9vz5PF4vCz+8cHrS/oseLv6Gaxn3z8SH8/0PeUdvXliMTXFk5HfCVmIDEP9kiuRpClyTTqNjpHBRbXLMz0hy/9ECgYEA/KB9HBzocRVU5nFxF5eiVKzevIjJQFMKNs6fCRXXZThu0gf/PBCFWHSxEAKTDcvj9mdcp81WXsN/e6gNAw+0O0/K2534ObnAcy3PMGoHNrvZgLk4iUtCC67xGnHi4oImfNEn9CHwStQ0sYt8jIhPPFBBhUX6ED+js/uGbY8/VukCgYEAu1lQiQzgT4ouzyOoy/rQARupx86KIYyo/I6/U+msHR/J3RkummAb4ub8dAGfTQTF3Jjl3BKmLUP6ncHg3EpPWghw3CIEaaKvyFU4CCifCmuErQx8YCmYXTJb5xOCJinF1aOSaw/HE8eOPvVQnSkoU87ES01tVGt4dotvgsny2yMCgYEAs/kdyEZr7gwVZOqSeA1Fz28sa0JDpbjDARKoSA+wWOMgSC78TW0zojXX4qEC5IRJzkQKxVzK2E4MZyrswi6Q5uRMj7L1oSJNEYEkJsiShRLEvCi4N09PKQWjrIRP77Bq/OcAwLLg/l45f/bwbym40S4Xz3tvz92WoWVienDf3ekCgYA+tHsXE98z4A0guU+yzgS2ijq+LGvhJMIenex9unUn3k7jGJ2Xf9l1jVgrv6tAzPsohWhRy1AhUGJeUNjhAmIiwTZ9B0mwzYnGJHe3i1kH1Mq4XLh4OxPLBaLq6YWjHlIf2jqUaNh5z03V1qefonnj6w2aIpUYL0xHaQ3umYcWPwKBgC8MtlLEfa9iBqFVkhxsSYDwMXdGare7nBHYnn0dRF1TNTvRTR4wLajWbx25PnH94WPyWtn38f/qaadloW8dJkrRKknqLqfjCXAYrMuxybjGnas8UrmL0Qumv26MOqvQamuwCbfgFA3KSNkKEqSmuGWAIZrqWLM5Rh+AfUOkIGCm" ], + "certificate" : [ "MIICmTCCAYECBgGGbwefzDANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVvYXNpczAeFw0yMzAyMjAxMzMxNTZaFw0zMzAyMjAxMzMzMzZaMBAxDjAMBgNVBAMMBW9hc2lzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuOFeqG2UdUaubpYhfM+BtrlXeO3SY1SGz/JfoT3ZImte2GfgGCEW9/PINjk7sWgd3tmD/M29XBu9CssMaETwj9XaTtc6mJwuAV8Weo5gOPwRQvIe+CcjFOgIlN8UWHt2oNisCCfmb07uHaprYc48MuriMRLvPpElaeYwDkhUGCEfKwbrHj/CnRXBY+IHA0uuxCQKLiYG5xx3yMV0wkEgNC6K4Wv3ZpUKM9SCjc5mDxX44imLErYpKs0+c08tp8Zif+fB3jVtQxOAg5+GMp0ANbQqZSBtVpmXp6mQEJBjBtyJ7oEOaEV7GgjkSF+D8zA47BVenhK6PRWryWj03kQ02wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAjdO9B2/mka4rDWYYLA/5unm//MQMZS2tEnvtN1CMfGiw7Cii6jJ7MavKRR55cMTM7ASRCBBxDyA09EZoYDb+vmVpPWnbfZ1IW555i3BiJhIgpx2FzspTyr5tpUHtqLXPPT0Yt7PbJdYtVvSYHw4GSA8k/Dk7PuVjkmaGzID2V68frjFFvrqBCqeyF5DPRogo6JGbOX3Kcs5agXx1R3RXj34hBjKhxFhzh61eBuFEghGeSjRcVgXomQTokA0d+OkQPsiwOEbSKjFuSv5fD7R3pOIYMuPiplzDFxWuAeiCq6b3pLrRsWWbN5MB4tupHsqqHPzi7IH8q8d5UKpYhXAIE" ], + "priority" : [ "100" ] + } + }, { + "id" : "f3040335-d218-4b26-beb4-73f764d474ed", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "a029c9d7-58d2-47e6-b641-e60328beef90" ], + "secret" : [ "gnimvojzrIwzEv_ehmzfXaotpHpg5BUSo1g6w3qmNUmFHAFuzx3rsIhqvTVLHXxvRwC7aCHjvkWKHXEcur4w4w" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "957c3fd1-aba2-4438-8a63-08c3f111bdb4", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "765ddd7f-548d-4760-b301-31851f30d1fc", + "alias" : "Authentication Options", + "description" : "Authentication options.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "basic-auth", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "basic-auth-otp", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "8fa36c33-c908-43b4-8129-2cbe00094e55", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "a1bc4ae4-2659-4bde-a189-ed2569654e8d", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "32b95adf-48fa-453d-98be-a4680bd7b663", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cc6da506-9dc0-4611-a2ba-f97c44470ad0", + "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", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "8b7f07c4-bcf9-46e1-afaf-93fd125a42e5", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "f2b684a0-0abc-43d0-9fb2-fe9395adcab7", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "cfceab8a-621b-426e-a87d-b2efb0e0a1b8", + "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", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "17a35dea-0549-4fbb-8766-c04b0ef5ead2", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "6b9a0d19-cdd0-4b90-840f-898361007444", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e8bdcec7-338d-4446-9e1e-0e0dcc796b83", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "dfc97434-b01e-40e0-90c0-4c698da9bdaf", + "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", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "d1bc6437-e891-49a4-baf0-6742db215ab7", + "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", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "31d5516e-a198-4d01-a48c-c0e68500733d", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "dc0dcc7f-8528-47cf-8283-b318ed9638ea", + "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", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Authentication Options", + "userSetupAllowed" : false + } ] + }, { + "id" : "bbd28157-8942-4ac9-8067-ed8b59bf476b", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "69edc775-74ca-43e0-aaba-102e349054c8", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "96d4f6df-98bb-4131-9e92-9ac7fe9c19d6", + "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", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "ccb3aaf5-1d4a-4487-83b1-e4c63f0dabce", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "f2caeab5-caae-43aa-9316-aa476a1658b7", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "1a4b4ad5-3b71-4fe4-8dcd-edcc9d02b282", + "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" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "20.0.3", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} diff --git a/oidc/keycloak/process-keycloak-templates.sh b/oidc/keycloak/process-keycloak-templates.sh new file mode 100755 index 0000000..afc0641 --- /dev/null +++ b/oidc/keycloak/process-keycloak-templates.sh @@ -0,0 +1,150 @@ +#!/bin/bash +set -e + +# ============================================================================ +# Keycloak Template Processor +# ============================================================================ +# Processes Keycloak realm and user templates with ___VAR___ placeholders +# Mimics Helm template processing from OasisPlatform Kubernetes deployment + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TEMPLATE_DIR="$SCRIPT_DIR" +OUTPUT_DIR="$SCRIPT_DIR/generated" +USERS_FILE="$SCRIPT_DIR/users.yaml" + +# Ensure required tools are available +if ! command -v jq &> /dev/null; then + echo " ERROR: jq is required but not installed" + exit 1 +fi + +if ! command -v uuidgen &> /dev/null; then + echo " ERROR: uuidgen is required but not installed" + exit 1 +fi + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# ============================================================================ +# Parse Users YAML +# ============================================================================ + +echo " -> Parsing users from users.yaml" + +# Convert YAML to JSON +# Try yq first, fallback to python +if command -v yq &> /dev/null; then + USERS_JSON=$(yq eval '.users' "$USERS_FILE" -o=json) +else + # Fallback to python + USERS_JSON=$(python3 -c " +import yaml, json, sys +with open('$USERS_FILE') as f: + data = yaml.safe_load(f) + print(json.dumps(data['users'])) +") +fi + +USER_COUNT=$(echo "$USERS_JSON" | jq '. | length') +echo " -> Found $USER_COUNT user(s)" + +if [ "$USER_COUNT" -eq 0 ]; then + echo " ERROR: No users defined in users.yaml" + exit 1 +fi + +# ============================================================================ +# Generate User JSON Entries +# ============================================================================ + +echo " -> Generating user entries from template" + +USER_TEMPLATE=$(cat "$TEMPLATE_DIR/oasis-realm-user.json.template") +USER_ENTRIES=() + +# Process each user +while IFS= read -r user; do + username=$(echo "$user" | jq -r '.username') + password=$(echo "$user" | jq -r '.password') + is_admin=$(echo "$user" | jq -r '.admin') + + echo " - Processing user: $username" + + # Generate UUID for user + uuid=$(uuidgen | tr '[:upper:]' '[:lower:]') + + # Build roles and groups + roles='"default-roles-oasis"' + groups='' + + if [ "$is_admin" = "true" ]; then + groups='"admin"' + fi + + # Replace variables in template + user_entry="$USER_TEMPLATE" + user_entry="${user_entry//___UUID___/$uuid}" + user_entry="${user_entry//___USERNAME___/$username}" + user_entry="${user_entry//___PASSWORD___/$password}" + user_entry="${user_entry//___ROLES___/$roles}" + user_entry="${user_entry//___GROUPS___/$groups}" + + USER_ENTRIES+=("$user_entry") + +done < <(echo "$USERS_JSON" | jq -c '.[]') + +# Join user entries with commas - use jq to properly format as JSON array +if [ ${#USER_ENTRIES[@]} -eq 1 ]; then + # Single user - no comma needed + USERS_JSON_ARRAY="${USER_ENTRIES[0]}" +else + # Multiple users - join with comma and newline + USERS_JSON_ARRAY=$(printf '%s,\n' "${USER_ENTRIES[@]}" | sed '$s/,$//') +fi + +echo " -> Generated ${#USER_ENTRIES[@]} user entry(ies)" + +# ============================================================================ +# Process Main Realm Template +# ============================================================================ + +echo " -> Processing realm template" + +REALM_TEMPLATE=$(cat "$TEMPLATE_DIR/oasis-realm.json.template") + +# Replace ___USERS___ placeholder (the placeholder is quoted: "___USERS___") +REALM_JSON="${REALM_TEMPLATE//\"___USERS___\"/$USERS_JSON_ARRAY}" + +# Build redirect base URL +REDIRECT_BASE="${OASIS_PROTOCOL}://${OASIS_UI_HOSTNAME}" +echo " -> Using redirect base: $REDIRECT_BASE" + +# Replace hostname placeholders (if any exist in the template) +REALM_JSON="${REALM_JSON//___OASIS_PROTOCOL___/$OASIS_PROTOCOL}" +REALM_JSON="${REALM_JSON//___OASIS_UI_HOSTNAME___/$OASIS_UI_HOSTNAME}" +REALM_JSON="${REALM_JSON//___REDIRECT_BASE___/$REDIRECT_BASE}" + +# ============================================================================ +# Write Output +# ============================================================================ + +OUTPUT_FILE="$OUTPUT_DIR/oasis-realm.json" +echo "$REALM_JSON" > "$OUTPUT_FILE" + +echo " -> Realm configuration written to: $OUTPUT_FILE" + +# ============================================================================ +# Validate JSON +# ============================================================================ + +echo " -> Validating generated JSON" + +if ! jq empty "$OUTPUT_FILE" 2>/dev/null; then + echo " ERROR: Generated JSON is invalid!" + echo " -> Check syntax errors in template or processing logic" + exit 1 +fi + +echo " OK Generated JSON is valid" +echo " OK Keycloak template processing complete!" diff --git a/oidc/keycloak/users.yaml b/oidc/keycloak/users.yaml new file mode 100644 index 0000000..ac0e681 --- /dev/null +++ b/oidc/keycloak/users.yaml @@ -0,0 +1,17 @@ +# Keycloak User Definitions +# These users will be created in the Keycloak realm during initialization +# Matches OasisPlatform Kubernetes values.yaml: keycloak.oasisRestApi.users + +users: + - username: admin + password: password + admin: true + + # Uncomment to add more users: + # - username: user + # password: password + # admin: false + # + # - username: analyst + # password: password + # admin: false diff --git a/requirements.txt b/requirements.txt index 31a3351..feddf34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,24 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile +# pip-compile requirements.in # -altair==5.5.0 +altair==6.0.0 # via streamlit anytree==2.13.0 # via oasislmf argparsetree==0.0.6 # via oasislmf -attrs==25.3.0 +attrs==25.4.0 # via # jsonschema # referencing blinker==1.9.0 # via streamlit -cachetools==6.1.0 +cachetools==6.2.6 # via streamlit -certifi==2025.7.14 +certifi==2026.1.4 # via # pyogrio # pyproj @@ -29,27 +29,27 @@ chardet==5.2.0 # via # oasislmf # ods-tools -charset-normalizer==3.4.2 +charset-normalizer==3.4.4 # via requests -click==8.2.1 +click==8.3.1 # via streamlit -cramjam==2.10.0 +cramjam==2.11.0 # via fastparquet -fastparquet==2024.11.0 +fastparquet==2025.12.0 # via # oasis-data-manager # oasislmf -fsspec==2025.7.0 +fsspec==2026.2.0 # via # fastparquet # oasis-data-manager -geopandas==1.1.1 +geopandas==1.1.2 # via -r requirements.in gitdb==4.0.12 # via gitpython -gitpython==3.1.45 +gitpython==3.1.46 # via streamlit -idna==3.10 +idna==3.11 # via requests jinja2==3.1.6 # via @@ -57,29 +57,29 @@ jinja2==3.1.6 # pydeck jsonref==1.1.0 # via ods-tools -jsonschema==4.25.0 +jsonschema==4.26.0 # via # altair # ods-tools -jsonschema-specifications==2025.4.1 +jsonschema-specifications==2025.9.1 # via jsonschema -llvmlite==0.44.0 +llvmlite==0.46.0 # via numba -markupsafe==3.0.2 +markupsafe==3.0.3 # via jinja2 -msgpack==1.1.1 +msgpack==1.1.2 # via oasislmf -narwhals==1.48.0 +narwhals==2.16.0 # via # altair # plotly -numba==0.61.2 +numba==0.63.1 # via # oasislmf # ods-tools -numexpr==2.11.0 +numexpr==2.14.1 # via oasislmf -numpy==2.2.6 +numpy==2.3.5 # via # fastparquet # geopandas @@ -92,15 +92,15 @@ numpy==2.2.6 # scipy # shapely # streamlit -oasis-data-manager==0.1.5 +oasis-data-manager==0.2.0 # via # oasislmf # ods-tools -oasislmf==2.4.8 +oasislmf==2.5.0 # via -r requirements.in -ods-tools==4.0.2 +ods-tools==5.0.1 # via oasislmf -packaging==25.0 +packaging==26.0 # via # altair # fastparquet @@ -109,7 +109,7 @@ packaging==25.0 # plotly # pyogrio # streamlit -pandas==2.3.1 +pandas==2.3.3 # via # -r requirements.in # fastparquet @@ -118,21 +118,22 @@ pandas==2.3.1 # oasislmf # ods-tools # streamlit -pillow==11.3.0 +pillow==12.1.1 # via streamlit -plotly==6.2.0 +plotly==6.5.2 # via -r requirements.in -protobuf==6.31.1 +protobuf==6.33.5 # via streamlit -pyarrow==21.0.0 +pyarrow==23.0.1 # via # oasislmf + # ods-tools # streamlit pydeck==0.9.1 # via streamlit -pyogrio==0.11.0 +pyogrio==0.12.1 # via geopandas -pyproj==3.7.1 +pyproj==3.7.2 # via geopandas python-dateutil==2.9.0.post0 # via pandas @@ -140,24 +141,26 @@ pytz==2025.2 # via # oasislmf # pandas -referencing==0.36.2 +referencing==0.37.0 # via # jsonschema # jsonschema-specifications -requests==2.32.4 +requests==2.32.5 # via # oasislmf # requests-toolbelt # streamlit requests-toolbelt==1.0.0 # via oasislmf -rpds-py==0.26.0 +rpds-py==0.30.0 # via # jsonschema # referencing -scipy==1.15.3 - # via oasislmf -shapely==2.1.1 +scipy==1.17.0 + # via + # oasislmf + # ods-tools +shapely==2.1.2 # via geopandas shutilwhich==1.1.0 # via oasislmf @@ -165,29 +168,35 @@ six==1.17.0 # via python-dateutil smmap==5.0.2 # via gitdb -streamlit==1.47.0 +streamlit==1.54.0 # via -r requirements.in tabulate==0.9.0 # via oasislmf -tblib==3.1.0 +tblib==3.2.2 # via oasislmf -tenacity==9.1.2 +tenacity==9.1.4 # via streamlit toml==0.10.2 # via streamlit -tornado==6.5.1 +tornado==6.5.4 # via streamlit -tqdm==4.67.1 - # via oasislmf -typing-extensions==4.14.1 +tqdm==4.67.3 + # via + # oasislmf + # ods-tools +typing-extensions==4.15.0 # via # altair # oasis-data-manager # referencing # streamlit -tzdata==2025.2 +tzdata==2025.3 # via pandas -urllib3==2.5.0 +urllib3==2.6.3 # via requests watchdog==6.0.0 # via streamlit +websocket-client==1.9.0 + # via oasislmf +xxhash==3.6.0 + # via oasis-data-manager diff --git a/scripts/add_test_portfolios.py b/scripts/add_test_portfolios.py index 0100226..d717d1f 100644 --- a/scripts/add_test_portfolios.py +++ b/scripts/add_test_portfolios.py @@ -8,6 +8,7 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) + def add_portfolio(client, input_args): existing_names = [r['name'] for r in client.portfolios.get().json()] logger.info('Adding portfolios...') @@ -18,6 +19,7 @@ def add_portfolio(client, input_args): logger.info(f'Adding {input_args["portfolio_name"]}') client.upload_inputs(**input_args) + def main(): parser = argparse.ArgumentParser(description='Script to add portfolios') parser.add_argument('-c', '--config', default='./portfolios.json', @@ -53,5 +55,6 @@ def main(): input_args = config[p] add_portfolio(client, input_args) -if __name__=="__main__": + +if __name__ == "__main__": main() diff --git a/scripts/model_registration.py b/scripts/model_registration.py new file mode 100644 index 0000000..5184206 --- /dev/null +++ b/scripts/model_registration.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Register an Oasis model via REST API. + +Mirrors kubernetes/charts/oasis-models/resources/model_registration.sh +for Docker Compose deployments. Run as a one-shot service before the +model worker starts so that run_register_worker_v2 finds the model +already registered and skips the Django admin user lookup. + +Required environment variables: + OASIS_API_URL Base API URL e.g. http://server:8000 + OASIS_MODEL_SUPPLIER_ID Model supplier ID + OASIS_MODEL_ID Model ID + OASIS_MODEL_VERSION_ID Model version ID + OASIS_RUN_MODE Run mode (v1 or v2) + OASIS_MODEL_DATA_DIRECTORY Directory containing model_settings.json + OASIS_SERVICE_USERNAME_OR_ID client_id (OIDC) or username (simple) + OASIS_SERVICE_PASSWORD_OR_SECRET client_secret (OIDC) or password (simple) + OASIS_USE_OIDC "true" for OIDC, empty/unset for simple JWT +""" + +import json +import os +import time + +import requests + +API_URL = os.environ["OASIS_API_URL"] +SUPPLIER = os.environ["OASIS_MODEL_SUPPLIER_ID"] +MODEL = os.environ["OASIS_MODEL_ID"] +VERSION = os.environ["OASIS_MODEL_VERSION_ID"] +RUN_MODE = os.environ["OASIS_RUN_MODE"] +MODEL_DATA_DIR = os.environ["OASIS_MODEL_DATA_DIRECTORY"] +SVC_ID = os.environ["OASIS_SERVICE_USERNAME_OR_ID"] +SVC_SECRET = os.environ["OASIS_SERVICE_PASSWORD_OR_SECRET"] +USE_OIDC = os.environ.get("OASIS_USE_OIDC", "").lower() == "true" + +settings_file = os.path.join(MODEL_DATA_DIR, "model_settings.json") + +print("\n=== Register model ===") +print(f" API URL : {API_URL}") +print(f" Supplier : {SUPPLIER}") +print(f" Model : {MODEL}") +print(f" Version : {VERSION}") +print(f" Run mode : {RUN_MODE}") + +# Authenticate +if USE_OIDC: + print(" Auth : OIDC client credentials") + auth_data = {"client_id": SVC_ID, "client_secret": SVC_SECRET} +else: + print(" Auth : simple JWT") + auth_data = {"username": SVC_ID, "password": SVC_SECRET} + +for attempt in range(1, 13): + resp = requests.post(f"{API_URL}/access_token/", json=auth_data) + if resp.ok: + break + if attempt < 12: + print(f" Auth failed ({resp.status_code}), retrying in 10s... ({attempt}/12)") + time.sleep(10) +else: + resp.raise_for_status() +token = resp.json()["access_token"] +headers = {"Authorization": f"Bearer {token}"} +print(" Authenticated successfully") + +# Check if model already exists +existing = requests.get( + f"{API_URL}/v2/models/", + params={"supplier_id": SUPPLIER, "version_id": VERSION}, + headers=headers, +).json() + +match = next( + (m for m in existing + if m["supplier_id"].lower() == SUPPLIER.lower() + and m["model_id"].lower() == MODEL.lower() + and m["version_id"].lower() == VERSION.lower()), + None, +) + +if match: + model_id = match["id"] + print(f" Model already registered (id={model_id})") +else: + print(" Model not found — registering") + resp = requests.post( + f"{API_URL}/v2/models/", + json={ + "supplier_id": SUPPLIER, + "model_id": MODEL, + "version_id": VERSION, + "run_mode": RUN_MODE.upper(), + }, + headers=headers, + ) + resp.raise_for_status() + model_id = resp.json()["id"] + print(f" Created model (id={model_id})") + +# Upload model settings +if os.path.exists(settings_file): + print(" Uploading model settings") + with open(settings_file) as f: + settings = json.load(f) + resp = requests.post( + f"{API_URL}/v2/models/{model_id}/settings/", + json=settings, + headers=headers, + ) + resp.raise_for_status() + print(" Model settings uploaded") +else: + print(f" WARNING: {settings_file} not found, skipping settings upload") + +print(" Done\n") diff --git a/scripts/utils.py b/scripts/utils.py index d4ca0a3..7936146 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=LOG_LEVEL) + def parse_initialise_args(**kwargs): max_retries = 1 if kwargs.get('retry'): @@ -27,18 +28,26 @@ def initialise_client(max_retries=1, retry_interval=5): Initialise APIClient with retries whilst waiting for the server to come online. ''' logger.info("Initialising client") - api_url = os.environ.get('API_URL', 'http://localhost:8000') - user = st.secrets.get('user', 'admin') - password = st.secrets.get('password', 'password') + api_url = os.environ.get('API_URL', 'http://ui.oasis.local/api') + auth_type = st.secrets.get('auth_type', 'simple') + if auth_type == "simple": + user = st.secrets.get('user', 'admin') + password = st.secrets.get('password', 'password') + else: # Use service client_credentials auth for OIDC + client_id = st.secrets.get('client_id', 'oasis-service') + client_secret = st.secrets.get('client_secret', 'serviceNotSoSecret') retry_count = 0 while True: try: - client = APIClient(api_url=api_url, username=user, password=password) + if auth_type == "simple": + client = APIClient(api_url=api_url, username=user, password=password, auth_type="simple") + else: + client = APIClient(api_url=api_url, client_id=client_id, client_secret=client_secret, auth_type="oidc") break except (ConnectionError, OasisException) as e: - logger.error(f'Retry: {retry_count+1}/{max_retries}.\nFailed to load client: {e}') + logger.error(f'Retry: {retry_count + 1}/{max_retries}.\nFailed to load client: {e}') retry_count += 1 diff --git a/scripts/validate-config.sh b/scripts/validate-config.sh new file mode 100755 index 0000000..65cd6ae --- /dev/null +++ b/scripts/validate-config.sh @@ -0,0 +1,203 @@ +#!/bin/bash +# Configuration validator for OasisPythonUI +# Validates .env file before deployment + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +ERRORS=0 +WARNINGS=0 + +echo "Validating configuration..." + +# ============================================================================ +# Check Required Variables +# ============================================================================ + +# Check API_AUTH_TYPE +if [ -z "$API_AUTH_TYPE" ]; then + echo "ERROR: API_AUTH_TYPE is not set" + ERRORS=$((ERRORS+1)) +elif [ "$API_AUTH_TYPE" != "simple" ] && [ "$API_AUTH_TYPE" != "keycloak" ] && [ "$API_AUTH_TYPE" != "authentik" ]; then + echo "ERROR: API_AUTH_TYPE must be 'simple', 'keycloak', or 'authentik' (got: $API_AUTH_TYPE)" + ERRORS=$((ERRORS+1)) +else + echo "OK API_AUTH_TYPE: $API_AUTH_TYPE" +fi + +# Check hostname +if [ -z "$OASIS_UI_HOSTNAME" ]; then + echo "ERROR: OASIS_UI_HOSTNAME is not set" + ERRORS=$((ERRORS+1)) +else + echo "OK OASIS_UI_HOSTNAME: $OASIS_UI_HOSTNAME" +fi + +# Check protocol +if [ -z "$OASIS_PROTOCOL" ]; then + echo "ERROR: OASIS_PROTOCOL is not set" + ERRORS=$((ERRORS+1)) +elif [ "$OASIS_PROTOCOL" != "http" ] && [ "$OASIS_PROTOCOL" != "https" ]; then + echo "ERROR: OASIS_PROTOCOL must be 'http' or 'https' (got: $OASIS_PROTOCOL)" + ERRORS=$((ERRORS+1)) +else + echo "OK OASIS_PROTOCOL: $OASIS_PROTOCOL" +fi + +# Check compose project name +if [ -z "$COMPOSE_PROJECT_NAME" ]; then + echo "WARNING: COMPOSE_PROJECT_NAME is not set (will use directory name)" + WARNINGS=$((WARNINGS+1)) +else + echo "OK COMPOSE_PROJECT_NAME: $COMPOSE_PROJECT_NAME" +fi + +# ============================================================================ +# Check Auth-Specific Configuration +# ============================================================================ + +if [ "$API_AUTH_TYPE" = "keycloak" ]; then + echo "" + echo "Checking Keycloak configuration..." + + # Check users file + if [ ! -f "$PROJECT_ROOT/oidc/keycloak/users.yaml" ]; then + echo "ERROR: oidc/keycloak/users.yaml not found" + ERRORS=$((ERRORS+1)) + else + echo "OK users.yaml found" + fi + + # Check template files + if [ ! -f "$PROJECT_ROOT/oidc/keycloak/oasis-realm.json.template" ]; then + echo "ERROR: oidc/keycloak/oasis-realm.json.template not found" + ERRORS=$((ERRORS+1)) + else + echo "OK realm template found" + fi + + if [ ! -f "$PROJECT_ROOT/oidc/keycloak/oasis-realm-user.json.template" ]; then + echo "ERROR: oidc/keycloak/oasis-realm-user.json.template not found" + ERRORS=$((ERRORS+1)) + else + echo "OK user template found" + fi + + # Check processing script + if [ ! -f "$PROJECT_ROOT/oidc/keycloak/process-keycloak-templates.sh" ]; then + echo "ERROR: oidc/keycloak/process-keycloak-templates.sh not found" + ERRORS=$((ERRORS+1)) + else + echo "OK processing script found" + fi + + # Check required variables + if [ -z "$KEYCLOAK_ADMIN_USER" ]; then + echo "ERROR: KEYCLOAK_ADMIN_USER is not set" + ERRORS=$((ERRORS+1)) + fi + + if [ -z "$OIDC_KEYCLOAK_CLIENT_NAME" ]; then + echo "ERROR: OIDC_KEYCLOAK_CLIENT_NAME is not set" + ERRORS=$((ERRORS+1)) + fi + +elif [ "$API_AUTH_TYPE" = "authentik" ]; then + echo "" + echo "Checking Authentik configuration..." + + # Check users file + if [ ! -f "$PROJECT_ROOT/oidc/authentik/users.yaml" ]; then + echo "ERROR: oidc/authentik/users.yaml not found" + ERRORS=$((ERRORS+1)) + else + echo "OK users.yaml found" + fi + + # Check template files + if [ ! -f "$PROJECT_ROOT/oidc/authentik/oasis-blueprint.yaml.template" ]; then + echo "ERROR: oidc/authentik/oasis-blueprint.yaml.template not found" + ERRORS=$((ERRORS+1)) + else + echo "OK blueprint template found" + fi + + if [ ! -f "$PROJECT_ROOT/oidc/authentik/oasis-users-blueprint.yaml.template" ]; then + echo "ERROR: oidc/authentik/oasis-users-blueprint.yaml.template not found" + ERRORS=$((ERRORS+1)) + else + echo "OK user template found" + fi + + # Check processing script + if [ ! -f "$PROJECT_ROOT/oidc/authentik/process-authentik-templates.sh" ]; then + echo "ERROR: oidc/authentik/process-authentik-templates.sh not found" + ERRORS=$((ERRORS+1)) + else + echo "OK processing script found" + fi + + # Check required variables + if [ -z "$AUTHENTIK_BOOTSTRAP_USER" ]; then + echo "ERROR: AUTHENTIK_BOOTSTRAP_USER is not set" + ERRORS=$((ERRORS+1)) + fi + + if [ -z "$OIDC_AUTHENTIK_CLIENT_NAME" ]; then + echo "ERROR: OIDC_AUTHENTIK_CLIENT_NAME is not set" + ERRORS=$((ERRORS+1)) + fi + +elif [ "$API_AUTH_TYPE" = "simple" ]; then + echo "" + echo "Checking Simple auth configuration..." + + if [ -z "$OASIS_ADMIN_USER" ]; then + echo "ERROR: OASIS_ADMIN_USER is not set" + ERRORS=$((ERRORS+1)) + fi + + if [ -z "$OASIS_ADMIN_PASS" ]; then + echo "WARNING: OASIS_ADMIN_PASS is not set" + WARNINGS=$((WARNINGS+1)) + fi +fi + +# ============================================================================ +# Check Database Configuration +# ============================================================================ + +echo "" +echo "Checking database configuration..." + +if [ -z "$OASIS_SERVER_DB_NAME" ]; then + echo "WARNING: OASIS_SERVER_DB_NAME is not set" + WARNINGS=$((WARNINGS+1)) +else + echo "OK OASIS_SERVER_DB_NAME: $OASIS_SERVER_DB_NAME" +fi + +if [ -z "$OASIS_CELERY_DB_NAME" ]; then + echo "WARNING: OASIS_CELERY_DB_NAME is not set" + WARNINGS=$((WARNINGS+1)) +else + echo "OK OASIS_CELERY_DB_NAME: $OASIS_CELERY_DB_NAME" +fi + +# ============================================================================ +# Summary +# ============================================================================ + +echo "" +echo "========================================" + +if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then + echo "Configuration validation passed!" + exit 0 +elif [ $ERRORS -eq 0 ]; then + echo "Configuration has $WARNINGS warning(s) but is valid" + exit 0 +else + echo "Configuration validation failed with $ERRORS error(s) and $WARNINGS warning(s)" + exit 1 +fi diff --git a/ui-config.json b/ui-config.json index d58559c..0555c45 100644 --- a/ui-config.json +++ b/ui-config.json @@ -8,5 +8,5 @@ "post_login_page": "pages/scenariosGuide.py", "model_map": { }, - "skip_login": true + "skip_login": false }