diff --git a/AI_CONTRIBUTING_RULES.md b/AI_CONTRIBUTING_RULES.md new file mode 100644 index 0000000..cee8d7e --- /dev/null +++ b/AI_CONTRIBUTING_RULES.md @@ -0,0 +1,13 @@ +# Global AI Agent Instructions (Jules) + +**1. The "Living Wiki" Mandate:** +This repository maintains its documentation as a living wiki. The primary sources of truth are the `SYSTEM_ARCHITECTURE.md` and `SECURITY_ARCHITECTURE.md` files. + +**2. Mandatory PR Checklist:** +For *every single Pull Request* you generate, regardless of the specific issue assigned to you, you MUST perform the following checks before committing: +* **Diff Review:** Analyze your proposed code changes. +* **Wiki Sync:** If your code alters the network flow, port configurations, authentication methods, database schemas, or deployment commands, you MUST open `SYSTEM_ARCHITECTURE.md` (and create it if it doesn't exist) to update the relevant sections to reflect your exact code changes. +* **Commit Inclusion:** The documentation updates must be included in the same commit/PR as the code changes. Do not submit a PR where the code and the architecture wiki are out of sync. + +**3. Global Build Constraint:** +Whenever generating or modifying Dockerfiles for this project, you MUST ensure the font package is explicitly set to `fonts-freefont-ttf` to prevent downstream rendering failures. diff --git a/README.md b/README.md index d586e32..b7da55e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This repository combines all the OSH modules and dependencies to deploy the OSH - [Java 21](https://www.oracle.com/java/technologies/downloads/#java21) - [Oakridge Build Node Repository](https://github.com/Botts-Innovative-Research/osh-oakridge-buildnode) - Node v22 +- [Docker](https://docs.docker.com/get-docker/) (Required to run the PostGIS database system) ## Installation Clone the repository and update all submodules recursively @@ -44,25 +45,63 @@ After the build completes, it can be located in `build/distributions/` Option 1: Command Line ```bash - unzip build/distributions/osh-node-oscar-1.0.zip - cd osh-node-oscar-1.0/osh-node-oscar-1.0 + # Note: Replace with the current version, e.g. 3.0.0 + unzip build/distributions/osh-node-oscar-.zip + cd osh-node-oscar-/osh-node-oscar- ``` ```bash - tar -xf build/distributions/osh-node-oscar-1.0.zip - cd osh-node-oscar-1.0/osh-node-oscar-1.0 + # Note: Replace with the current version, e.g. 3.0.0 + tar -xf build/distributions/osh-node-oscar-.zip + cd osh-node-oscar-/osh-node-oscar- ``` Option 2: Use File Explorer 1. Navigate to `path/to/osh-oakridge-buildnode/build/distributions/` - 2. Right-click `osh-node-oscar-1.0.zip`. + 2. Right-click `osh-node-oscar-.zip` (where `` is the current release version, e.g. `3.0.0`). 3. Select **Extract All..** 4. Choose your destination, (or leave the default) and extract. -1. Launch the OSH node: - Run the launch script, "launch.sh" for linux/mac and "launch.bat" for windows. +1. Launch the OSH node and PostGIS Database: + The database management system is handled through Docker. The default launch scripts automatically build and run a PostGIS container using the `Dockerfile` located in `dist/release/postgis`, and then start the OSH node. + Run the launch script, `launch-all.sh` (or `launch.sh` within the `osh-node-oscar` folder directly if the database is already running) for linux, `launch-all-arm.sh` (or `launch-arm.sh` if it exists) for mac, and `launch-all.bat` (or `launch.bat`) for windows. 2. Access the OSH Node - Remote: **[ip-address]:8282/sensorhub/admin** - Locally: **http://localhost:8282/sensorhub/admin** -The default credentials to access the OSH Node are admin:admin. This can be changed in the Security section of the admin page. +### First-Time Setup +On first boot, OSCAR enters an **Uninitialized State** and requires configuration via a Setup Wizard. +1. Navigate to `http://localhost:8282/` or `http://localhost:8282/sensorhub/admin`. +2. You will be automatically redirected to the **Setup Wizard**. +3. **Create an Admin Password**: Set a strong password for the `admin` account. +4. **Configure TOTP**: + - Scan the displayed QR code with an authenticator app (Google Authenticator, Authy, etc.). + - **Important**: Save the secret key shown in the wizard! + - Use the **Test Code** form to verify your setup before proceeding. +5. Once complete, you will be redirected to the Admin UI login. + +### Logging In +After initialization, use the following credentials: +- **Username**: `admin` +- **Password**: The password you set during the Setup Wizard. +- **Two-Factor Authentication**: + - If your browser or client supports it, enter your password as usual and provide the 6-digit TOTP code when prompted. + - If you are prompted for a single login by the browser and can't provide a TOTP code separately, enter your password followed by a colon and the code (e.g., `mypassword:123456`). + +**Language Selection** +The user can select different languages for the Admin UI by using the language drop-down menu located in the top right corner of the Admin UI toolbar. Selecting a new language will instantly switch the UI localization. + +**Two-Factor Authentication (2FA)** +2FA is mandatory for the administrator account and can be configured for other users to add an extra layer of security. To set this up for additional users: +1. Log in to the Admin UI as `admin`. +2. Navigate to the **Security** section. +3. Edit a user profile and set up Two-Factor Authentication. A popup window will appear with a QR code generated locally on the server. +4. Scan the QR code with an authenticator app (like Google Authenticator or Authy) to complete the setup. + +**Importing/Exporting Lane Configurations via CSV** +Configurations for Lane Systems can be bulk managed via spreadsheet (CSV). +1. Log in to the Admin UI. +2. Navigate to **Services -> OSCAR Service**. +3. Within the configuration form for the OSCAR service, locate the property for spreadsheet configuration (`spreadsheetConfigPath`). +4. To export, click the download button to retrieve the current configurations as a CSV file. +5. To import, upload your modified CSV file through the provided upload mechanism in the service configuration to apply new or updated lane setups. For documentation on configuring a Lane System on the OSH Admin panel, please refer to the OSCAR Documentation provided in the Google Drive documentation folder. diff --git a/SECURITY_ARCHITECTURE.md b/SECURITY_ARCHITECTURE.md new file mode 100644 index 0000000..bb13f4c --- /dev/null +++ b/SECURITY_ARCHITECTURE.md @@ -0,0 +1,46 @@ +# OSCAR Security Hardening Architecture + +**Critical Domain Context:** +This project is an Open Source Central Alarm Station (OSCAR) monitoring radiation portal monitors. The application runs cross-platform on Windows, macOS, and Linux. The primary critical threat is the unauthorized suppression, modification, or spoofing of alarms. Note this specific nomenclature: +* **G Alarm:** Gamma Alarm. +* **N Alarm:** Neutron Alarm. +* **G-N:** Gamma Neutron Alarm. + +**OpenSensorHub (OSH) Ecosystem Constraint:** +OSCAR is built on the OpenSensorHub framework. **Under no circumstances may any code modifications break compatibility with the larger OSH ecosystem.** * Standard OGC SWE, SOS, and SPS API endpoints must remain fully compliant. +* Sensor drivers (e.g., video processing, hardware interfaces mapped in `config.csv`) must not be prevented from initializing or communicating. +* Machine-to-machine API routes cannot rely on human-interactive authentication (like 302 redirects to a TOTP login). + +**Global Build Constraint:** +Whenever generating or modifying Dockerfiles for this project, you MUST ensure the font package is explicitly set to `fonts-freefont-ttf`. This is strictly required to prevent downstream rendering failures in the application's graphical reporting components. + +## Database Security Implementation + +### SCRAM-SHA-256 Authentication +PostgreSQL is configured to enforce `scram-sha-256` authentication for all database users. This is initialized during the PostGIS container setup via `POSTGRES_INITDB_ARGS`. + +### Docker Secrets for Database Credentials +The system uses Docker Secrets (via bind mounts) to manage database passwords. +- **Injected Secret Path**: `/run/secrets/db_password` within the container. +- **Environment Variable**: `POSTGRES_PASSWORD_FILE` points to this secret path. +- **Backend Priority**: The OSH Java backend is architected to prioritize the `POSTGRES_PASSWORD_FILE` environment variable during initialization, overriding any plaintext credentials in `config.json`. + +### Configurable Networking and TLS +- **DB Host**: The database host is configurable via the `DB_HOST` environment variable (default: `localhost`), enabling secure deployment on separate LAN machines. +- **TLS Enforcement**: All connections from the OSH backend to PostGIS are secured over TLS. This is enforced by using `sslmode=require` in the JDBC connection string in the `ConnectionManager`. + +## Application-Level Security Hardening + +### Ephemeral CA and TLS Certificates +On first boot, the system generates an ephemeral Root CA and a Leaf TLS certificate. +- **Root CA Private Key**: Held strictly in memory during the generation of the leaf certificate and never persisted to disk. +- **Leaf Certificate**: Stored in a PKCS12 keystore (`osh-keystore.p12`). +- **Key Storage Security**: The keystore password is automatically generated and stored in a hidden `.app_secrets` file. Access to this file and the keystore is restricted to the executing user using POSIX permissions (Linux/macOS) or ACLs (Windows). +- **Public CA Download**: The public Root CA certificate is available for download at `/sensorhub/admin/ca-cert` to allow clients to establish trust. + +### Setup Wizard and Credential Management +The system does not ship with default administrative credentials. +- **Uninitialized State**: If the system detects that it has not been configured (no admin password set), it enters an uninitialized state. +- **Mandatory Redirection**: In the uninitialized state, all requests to the root URL or Admin UI are redirected to a Setup Wizard. +- **Initialization**: The Setup Wizard forces the creation of a strong admin password (hashed using PBKDF2) and initializes the TOTP 2FA seed. +- **Bridged Session Authentication**: To prevent repeated authentication prompts between isolated Jetty contexts (e.g., the root Viewer and the `/sensorhub` Admin UI), the system implements a session bridging mechanism. Validated 2FA sessions are registered in a global registry and propagated across contexts using a `BridgedAuthenticator` and manual cookie header parsing. diff --git a/SYSTEM_ARCHITECTURE.md b/SYSTEM_ARCHITECTURE.md new file mode 100644 index 0000000..c9f7252 --- /dev/null +++ b/SYSTEM_ARCHITECTURE.md @@ -0,0 +1,42 @@ +# OSCAR System Architecture + +## Overview +OSCAR (Open Source Central Alarm Station) is a monitoring system for radiation portal monitors based on the OpenSensorHub (OSH) framework. + +## Component Network Flow and Ports + +### Components: +- **OSH Backend**: Java-based core application. +- **PostGIS Database**: PostgreSQL with PostGIS extensions for persistent storage. +- **Client Web UI**: React/Frontend viewer. + +### Default Port Configuration: +- **OSH Backend API (HTTP)**: `8282` +- **OSH Backend Admin UI**: `8282` +- **PostGIS Database**: `5432` +- **MQTT Server (HiveMQ)**: WebSockets on `/mqtt` (via proxy on port `8282`) + +### Network Flows: +- **Client to OSH**: Clients interact with OSH through its REST API and Web UI on port `8282`. +- **OSH to PostGIS**: The OSH backend connects to the PostGIS database over the network (local or LAN) on port `5432`. This connection is secured via TLS and authenticated with SCRAM-SHA-256. + +## Deployment and Lifecycle Commands + +### Main Launch Scripts: +Located in `dist/release/`: +- `launch-all.sh`: Starts the PostGIS container and the OSH backend (Linux/macOS). +- `launch-all-arm.sh`: Starts the PostGIS container and the OSH backend (ARM64, e.g., Mac M1/M2/M3). +- `launch-all.bat`: Starts the PostGIS container and the OSH backend (Windows). + +### Standalone Database Scripts: +Located in `dist/release/postgis/`: +- `run-postgis.sh`: Starts the PostGIS container independently (Linux/macOS). +- `run-postgis-arm.sh`: Starts the PostGIS container independently (ARM64). +- `run-postgis.bat`: Starts the PostGIS container independently (Windows). + +## Database Utilities +Cross-platform scripts are provided in the repository root for maintenance: +- `backup.sh/bat`: Safely creates a database dump. +- `restore.sh/bat`: Restores the database from a dump. + +These utilities respect the `DB_HOST` and `POSTGRES_PASSWORD_FILE` environment variables. diff --git a/backup.bat b/backup.bat new file mode 100644 index 0000000..8c62a77 --- /dev/null +++ b/backup.bat @@ -0,0 +1,32 @@ +@echo off +setlocal enabledelayedexpansion + +if "%DB_HOST%"=="" (set DB_HOST=localhost) +set DB_NAME=gis +set DB_USER=postgres + +if "%POSTGRES_PASSWORD_FILE%"=="" ( + echo Error: POSTGRES_PASSWORD_FILE environment variable is not set. + exit /b 1 +) + +if not exist "%POSTGRES_PASSWORD_FILE%" ( + echo Error: Password file %POSTGRES_PASSWORD_FILE% does not exist. + exit /b 1 +) + +set /p PGPASSWORD=<"%POSTGRES_PASSWORD_FILE%" + +set TIMESTAMP=%date:~10,4%%date:~4,2%%date:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2% +set TIMESTAMP=%TIMESTAMP: =0% +set BACKUP_FILE=backup_%TIMESTAMP%.dump + +echo Backing up database %DB_NAME% from %DB_HOST%... +pg_dump -h %DB_HOST% -U %DB_USER% -d %DB_NAME% -F c -f "%BACKUP_FILE%" + +if %errorlevel% equ 0 ( + echo Backup completed successfully: %BACKUP_FILE% +) else ( + echo Backup failed. + exit /b 1 +) diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..cf8f55e --- /dev/null +++ b/backup.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +DB_HOST="${DB_HOST:-localhost}" +DB_NAME="gis" +DB_USER="postgres" + +if [ -z "$POSTGRES_PASSWORD_FILE" ]; then + echo "Error: POSTGRES_PASSWORD_FILE environment variable is not set." + exit 1 +fi + +if [ ! -f "$POSTGRES_PASSWORD_FILE" ]; then + echo "Error: Password file $POSTGRES_PASSWORD_FILE does not exist." + exit 1 +fi + +export PGPASSWORD=$(cat "$POSTGRES_PASSWORD_FILE") + +echo "Backing up database $DB_NAME from $DB_HOST..." +pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -F c -f "backup_$(date +%Y%m%d_%H%M%S).dump" + +if [ $? -eq 0 ]; then + echo "Backup completed successfully." +else + echo "Backup failed." + exit 1 +fi diff --git a/build.gradle b/build.gradle index 3f4a802..20aad5e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ apply from: gradle.oshCoreDir + '/common.gradle' description = '' allprojects { - version = "3.0.0-rc.5" + version = "3.0.0" } subprojects { diff --git a/changelog.md b/changelog.md index 4564fab..8696d47 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # OSCAR Build Node Change Log All notable changes to this project will be documented in this file. +## 3.0.0 2026-02-04 +This is the official first release of 3.0.0 +### Changes +- Data from database is purged regularly with "daily files" exported at midnight +- Added internationalization (i18n) to the frontend +- Sorted lanes by alphanumeric order in the frontend dashboard +- Use server-side filters in frontend tables +### Fixed +- Fixed issue where database is queried everytime Admin UI is loaded ## 3.0.0-rc.5 2025-12-11 ### Changes diff --git a/dist/config/standard/config.json b/dist/config/standard/config.json index a88eea6..18b41a6 100644 --- a/dist/config/standard/config.json +++ b/dist/config/standard/config.json @@ -27,9 +27,7 @@ "roles": [ "admin" ], - "allow": [ - "fileserver[af72442c-1ce6-4baa-a126-ed41dda26910]" - ], + "allow": [], "deny": [] }, { @@ -94,7 +92,7 @@ "uiClass": "com.botts.ui.oscar.forms.OSCARServiceForm" } ], - "deploymentName": "OSCAR 3.0.0-rc.5", + "deploymentName": "OSCAR 3.0.0", "enableLandingPage": false, "id": "5cb05c9c-9123-4fa1-8731-ffaa51489678", "autoStart": true, @@ -158,7 +156,8 @@ "initialBuckets": [ "sitemap", "reports", - "videos" + "videos", + "adjudication" ], "fileStoreRootDir": "files", "endPoint": "/buckets", @@ -174,7 +173,7 @@ "url": "localhost:5432", "dbName": "gis", "login": "postgres", - "password": "postgres", + "password": "", "idProviderType": "SEQUENTIAL", "autoCommitPeriod": 10, "useBatch": false, diff --git a/dist/release/launch-all-arm.sh b/dist/release/launch-all-arm.sh index 96c1cc5..a14b151 100755 --- a/dist/release/launch-all-arm.sh +++ b/dist/release/launch-all-arm.sh @@ -1,6 +1,6 @@ #!/bin/bash -HOST=localhost +HOST="${DB_HOST:-localhost}" DB_NAME=gis DB_USER=postgres RETRY_MAX=20 @@ -8,6 +8,16 @@ RETRY_INTERVAL=5 PROJECT_DIR="$(pwd)" # Store the original directory CONTAINER_NAME=oscar-postgis-container +# Set up DB password secret +if [ -z "$POSTGRES_PASSWORD_FILE" ]; then + export POSTGRES_PASSWORD_FILE="${PROJECT_DIR}/.db_password" +fi + +if [ ! -f "$POSTGRES_PASSWORD_FILE" ]; then + echo "Generating new database password..." + openssl rand -base64 32 > "$POSTGRES_PASSWORD_FILE" +fi + #sudo docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # Create pgdata directory if needed @@ -48,10 +58,11 @@ else --name $CONTAINER_NAME \ -e POSTGRES_DB=$DB_NAME \ -e POSTGRES_USER=$DB_USER \ - -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_PASS=$(cat "$POSTGRES_PASSWORD_FILE") \ -e DATADIR=/var/lib/postgresql/data \ -p 5432:5432 \ -v "$(pwd)/pgdata:/var/lib/postgresql/data" \ + -v "$POSTGRES_PASSWORD_FILE:/run/secrets/db_password" \ -d \ oscar-postgis-arm || { echo "Failed to start PostGIS container"; exit 1; } fi @@ -60,16 +71,24 @@ fi echo "Waiting for PostGIS ARM64 (PostgreSQL) to be ready..." RETRY_COUNT=0 -export PGPASSWORD=postgres # Needed for pg_isready with password - -until docker exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" > /dev/null 2>&1; do +until docker exec -u "$DB_USER" "$CONTAINER_NAME" pg_isready -d "$DB_NAME" > /dev/null 2>&1; do echo "PostGIS not ready yet, retrying..." sleep "${RETRY_INTERVAL}" done echo "PostGIS (PostgreSQL) is ready! Please wait for OpenSensorHub to start..." -sleep 10 +sleep 30 + +# Final check +until docker exec -u "$DB_USER" "$CONTAINER_NAME" pg_isready -d "$DB_NAME" > /dev/null 2>&1; do + echo "PostGIS still restarting, waiting..." + sleep 5 +done + +# Export for OSH backend +export DB_HOST="$HOST" +export POSTGRES_PASSWORD_FILE="$POSTGRES_PASSWORD_FILE" # Launch osh-node-oscar cd "$PROJECT_DIR/osh-node-oscar" || { echo "Error: osh-node-oscar not found"; exit 1; } diff --git a/dist/release/launch-all.bat b/dist/release/launch-all.bat index 5c93081..1daae2e 100755 --- a/dist/release/launch-all.bat +++ b/dist/release/launch-all.bat @@ -2,7 +2,7 @@ setlocal enabledelayedexpansion REM ==== CONFIG ==== -set HOST=localhost +if "%DB_HOST%"=="" (set HOST=localhost) else (set HOST=%DB_HOST%) set PORT=5432 set DB_NAME=gis set USER=postgres @@ -14,6 +14,16 @@ set IMAGE_NAME=oscar-postgis echo PROJECT_DIR is: %PROJECT_DIR% +REM Set up DB password secret +if "%POSTGRES_PASSWORD_FILE%"=="" (set "POSTGRES_PASSWORD_FILE=%PROJECT_DIR%\.db_password") + +if not exist "%POSTGRES_PASSWORD_FILE%" ( + echo Generating new database password... + powershell -Command "$p = New-Object byte[] 32; (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($p); $pwd = [Convert]::ToBase64String($p); [System.IO.File]::WriteAllText(\"%POSTGRES_PASSWORD_FILE:\=\\%\", $pwd)" +) + +set /p DB_PASSWORD=<"%POSTGRES_PASSWORD_FILE%" + where docker >nul 2>&1 if %errorlevel% neq 0 ( echo ERROR: Docker is not installed or not in PATH. @@ -61,9 +71,10 @@ if defined CONTAINER_EXISTS ( --name %CONTAINER_NAME% ^ -e POSTGRES_DB=%DB_NAME% ^ -e POSTGRES_USER=%USER% ^ - -e POSTGRES_PASSWORD=postgres ^ + -e POSTGRES_PASSWORD_FILE=/run/secrets/db_password ^ -p %PORT%:5432 ^ -v "%PROJECT_DIR%\pgdata:/var/lib/postgresql/data" ^ + -v "%POSTGRES_PASSWORD_FILE%:/run/secrets/db_password" ^ -d ^ %IMAGE_NAME% @@ -78,7 +89,7 @@ echo Waiting for PostGIS database to become ready... set RETRY_COUNT=0 :wait_loop -docker exec %CONTAINER_NAME% pg_isready -U %USER% -d %DB_NAME% >nul 2>&1 +docker exec -u %USER% %CONTAINER_NAME% pg_isready -d %DB_NAME% >nul 2>&1 if %errorlevel% equ 0 ( echo Received OK from PostGIS. Please wait for initialization... goto after_wait @@ -97,10 +108,25 @@ goto wait_loop :after_wait -timeout /t 10 >nul +timeout /t 30 >nul + +:final_wait_loop +docker exec -u %USER% %CONTAINER_NAME% pg_isready -d %DB_NAME% >nul 2>&1 +if %errorlevel% equ 0 ( + goto after_final_wait +) +echo PostGIS still restarting, waiting... +timeout /t 5 >nul +goto final_wait_loop + +:after_final_wait echo PostGIS database is ready! +REM Export for OSH backend +set DB_HOST=%HOST% +set POSTGRES_PASSWORD_FILE=%POSTGRES_PASSWORD_FILE% + cd "%PROJECT_DIR%\osh-node-oscar" if %errorlevel% neq 0 ( echo ERROR: osh-node-oscar directory not found. diff --git a/dist/release/launch-all.sh b/dist/release/launch-all.sh index 5716c7c..9ca8e4f 100755 --- a/dist/release/launch-all.sh +++ b/dist/release/launch-all.sh @@ -1,6 +1,6 @@ #!/bin/bash -HOST="localhost" +HOST="${DB_HOST:-localhost}" PORT="5432" DB_NAME="gis" DB_USER="postgres" @@ -9,6 +9,16 @@ RETRY_INTERVAL=5 PROJECT_DIR="$(pwd)" # Store the original directory CONTAINER_NAME="oscar-postgis-container" +# Set up DB password secret +if [ -z "$POSTGRES_PASSWORD_FILE" ]; then + export POSTGRES_PASSWORD_FILE="${PROJECT_DIR}/.db_password" +fi + +if [ ! -f "$POSTGRES_PASSWORD_FILE" ]; then + echo "Generating new database password..." + openssl rand -base64 32 > "$POSTGRES_PASSWORD_FILE" +fi + #docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # Create pgdata directory if needed @@ -51,9 +61,10 @@ else --name "$CONTAINER_NAME" \ -e POSTGRES_DB="$DB_NAME" \ -e POSTGRES_USER="$DB_USER" \ - -e POSTGRES_PASSWORD="postgres" \ + -e POSTGRES_PASSWORD_FILE="/run/secrets/db_password" \ -p $PORT:5432 \ -v "${PROJECT_DIR}/pgdata:/var/lib/postgresql/data" \ + -v "$POSTGRES_PASSWORD_FILE:/run/secrets/db_password" \ -d \ oscar-postgis || { echo "Failed to start PostGIS container"; exit 1; } fi @@ -62,16 +73,24 @@ fi echo "Waiting for PostGIS (PostgreSQL) to be ready..." RETRY_COUNT=0 -export PGPASSWORD=postgres # Needed for pg_isready with password - -until docker exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" > /dev/null 2>&1; do +until docker exec -u "$DB_USER" "$CONTAINER_NAME" pg_isready -d "$DB_NAME" > /dev/null 2>&1; do echo "PostGIS not ready yet, retrying..." sleep "${RETRY_INTERVAL}" done echo "PostGIS (PostgreSQL) is ready! Please wait for OpenSensorHub to start..." -sleep 10 +sleep 30 + +# Final check +until docker exec -u "$DB_USER" "$CONTAINER_NAME" pg_isready -d "$DB_NAME" > /dev/null 2>&1; do + echo "PostGIS still restarting, waiting..." + sleep 5 +done + +# Export for OSH backend +export DB_HOST="$HOST" +export POSTGRES_PASSWORD_FILE="$POSTGRES_PASSWORD_FILE" # Launch osh-node-oscar cd "$PROJECT_DIR/osh-node-oscar" || { echo "Error: osh-node-oscar not found"; exit 1; } diff --git a/dist/release/postgis/Dockerfile b/dist/release/postgis/Dockerfile index ce645c7..090489a 100644 --- a/dist/release/postgis/Dockerfile +++ b/dist/release/postgis/Dockerfile @@ -1,5 +1,14 @@ FROM postgis/postgis:16-3.4 +# Install fonts as required by AI_CONTRIBUTING_RULES.md +RUN apt-get update && apt-get install -y fonts-freefont-ttf && rm -rf /var/lib/apt/lists/* + COPY init-extensions.sql /docker-entrypoint-initdb.d/init-extensions.sql -ENV POSTGRES_INITDB_ARGS="-c max_parallel_workers_per_gather=0 -c max_parallel_workers=0" \ No newline at end of file +# Generate self-signed certificate for SSL support +RUN openssl req -new -x509 -days 365 -nodes -text -out /var/lib/postgresql/server.crt \ + -keyout /var/lib/postgresql/server.key -subj "/CN=oscar-postgis" \ + && chmod 600 /var/lib/postgresql/server.key \ + && chown postgres:postgres /var/lib/postgresql/server.key /var/lib/postgresql/server.crt + +ENV POSTGRES_INITDB_ARGS="--auth-local=trust --auth-host=scram-sha-256 -c max_parallel_workers_per_gather=0 -c max_parallel_workers=0 -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key" \ No newline at end of file diff --git a/dist/release/postgis/Dockerfile-arm64 b/dist/release/postgis/Dockerfile-arm64 index becfa20..4efddd7 100644 --- a/dist/release/postgis/Dockerfile-arm64 +++ b/dist/release/postgis/Dockerfile-arm64 @@ -1,7 +1,14 @@ FROM kartoza/postgis:16-3.4 -RUN echo "host all all all md5" >> /etc/postgresql/16/main/pg_hba.conf +# Install fonts as required by AI_CONTRIBUTING_RULES.md +RUN apt-get update && apt-get install -y fonts-freefont-ttf && rm -rf /var/lib/apt/lists/* COPY init-extensions.sql /docker-entrypoint-initdb.d/init-extensions.sql -ENV POSTGRES_INITDB_ARGS="-c max_parallel_workers_per_gather=0 -c max_parallel_workers=0" \ No newline at end of file +# Generate self-signed certificate for SSL support +RUN openssl req -new -x509 -days 365 -nodes -text -out /var/lib/postgresql/server.crt \ + -keyout /var/lib/postgresql/server.key -subj "/CN=oscar-postgis" \ + && chmod 600 /var/lib/postgresql/server.key \ + && chown postgres:postgres /var/lib/postgresql/server.key /var/lib/postgresql/server.crt + +ENV POSTGRES_INITDB_ARGS="--auth-local=trust --auth-host=scram-sha-256 -c max_parallel_workers_per_gather=0 -c max_parallel_workers=0 -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key" \ No newline at end of file diff --git a/dist/release/postgis/init-extensions.sql b/dist/release/postgis/init-extensions.sql index 796b363..50f33ee 100644 --- a/dist/release/postgis/init-extensions.sql +++ b/dist/release/postgis/init-extensions.sql @@ -1,4 +1,8 @@ ALTER SYSTEM SET max_connections = 1024; +ALTER SYSTEM SET ssl = 'on'; +ALTER SYSTEM SET ssl_cert_file = '/var/lib/postgresql/server.crt'; +ALTER SYSTEM SET ssl_key_file = '/var/lib/postgresql/server.key'; + \connect gis; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS btree_gist; diff --git a/dist/release/postgis/run-postgis-arm.sh b/dist/release/postgis/run-postgis-arm.sh index 62ff5b6..987aa40 100755 --- a/dist/release/postgis/run-postgis-arm.sh +++ b/dist/release/postgis/run-postgis-arm.sh @@ -5,13 +5,26 @@ if [ ! -d "$(pwd)/pgdata" ]; then mkdir -p "$(pwd)/pgdata" fi +# Set up DB password secret +PROJECT_DIR="$(pwd)" +if [ -z "$POSTGRES_PASSWORD_FILE" ]; then + export POSTGRES_PASSWORD_FILE="${PROJECT_DIR}/.db_password" +fi + +if [ ! -f "$POSTGRES_PASSWORD_FILE" ]; then + echo "Generating new database password..." + openssl rand -base64 32 > "$POSTGRES_PASSWORD_FILE" +fi + docker build . --file=Dockerfile-arm64 --tag=oscar-postgis-arm docker run \ -e PG_MAX_CONNECTIONS=500 \ -e POSTGRES_DB=gis \ -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_PASS=$(cat "$POSTGRES_PASSWORD_FILE") \ -e DATADIR=/var/lib/postgresql/data \ -p 5432:5432 \ -v "$(pwd)/pgdata:/var/lib/postgresql/data" \ + -v "$POSTGRES_PASSWORD_FILE:/run/secrets/db_password" \ + -d \ oscar-postgis-arm \ No newline at end of file diff --git a/dist/release/postgis/run-postgis.bat b/dist/release/postgis/run-postgis.bat index f7ae6ad..d09ddb6 100644 --- a/dist/release/postgis/run-postgis.bat +++ b/dist/release/postgis/run-postgis.bat @@ -5,6 +5,14 @@ if not exist "%cd%\pgdata" ( mkdir "%cd%\pgdata" ) +# Set up DB password secret +if "%POSTGRES_PASSWORD_FILE%"=="" (set POSTGRES_PASSWORD_FILE=%cd%\.db_password) + +if not exist "%POSTGRES_PASSWORD_FILE%" ( + echo Generating new database password... + powershell -Command "$p = New-Object byte[] 32; (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($p); $pwd = [Convert]::ToBase64String($p); [System.IO.File]::WriteAllText('%POSTGRES_PASSWORD_FILE%', $pwd)" +) + docker build . --tag=oscar-postgis docker run ^ @@ -13,7 +21,9 @@ docker run ^ -e PG_MAX_CONNECTIONS=500 ^ -e POSTGRES_DB=gis ^ -e POSTGRES_USER=postgres ^ - -e POSTGRES_PASSWORD=postgres ^ + -e POSTGRES_PASSWORD_FILE=/run/secrets/db_password ^ -p 5432:5432 ^ -v "%cd%\pgdata:/var/lib/postgresql/data" ^ + -v "%POSTGRES_PASSWORD_FILE%:/run/secrets/db_password" ^ + -d ^ oscar-postgis \ No newline at end of file diff --git a/dist/release/postgis/run-postgis.sh b/dist/release/postgis/run-postgis.sh index 4b47008..4ff1726 100755 --- a/dist/release/postgis/run-postgis.sh +++ b/dist/release/postgis/run-postgis.sh @@ -5,12 +5,25 @@ if [ ! -d "$(pwd)/pgdata" ]; then mkdir -p "$(pwd)/pgdata" fi +# Set up DB password secret +PROJECT_DIR="$(pwd)" +if [ -z "$POSTGRES_PASSWORD_FILE" ]; then + export POSTGRES_PASSWORD_FILE="${PROJECT_DIR}/.db_password" +fi + +if [ ! -f "$POSTGRES_PASSWORD_FILE" ]; then + echo "Generating new database password..." + openssl rand -base64 32 > "$POSTGRES_PASSWORD_FILE" +fi + docker build . --file=Dockerfile --tag=oscar-postgis docker run \ -e PG_MAX_CONNECTIONS=1024 \ -e POSTGRES_DB=gis \ -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_PASSWORD_FILE="/run/secrets/db_password" \ -p 5432:5432 \ -v "$(pwd)/pgdata:/var/lib/postgresql/data" \ + -v "$POSTGRES_PASSWORD_FILE:/run/secrets/db_password" \ + -d \ oscar-postgis diff --git a/dist/scripts/standard/launch.bat b/dist/scripts/standard/launch.bat index 52eb997..b6fb093 100755 --- a/dist/scripts/standard/launch.bat +++ b/dist/scripts/standard/launch.bat @@ -7,33 +7,54 @@ CALL %~dp0load_trusted_certs.bat set KEYSTORE=.\osh-keystore.p12 set KEYSTORE_TYPE=PKCS12 -set KEYSTORE_PASSWORD=atakatak + +REM Ephemeral CA Generation +if not exist "%KEYSTORE%" ( + echo Generating ephemeral certificates... + java -cp "lib/*" com.botts.impl.security.EphemeralCAUtility +) + +if exist ".app_secrets" ( + set /p KEYSTORE_PASSWORD=<.app_secrets +) else ( + set KEYSTORE_PASSWORD=atakatak +) set TRUSTSTORE=.\truststore.jks set TRUSTSTORE_TYPE=JKS set TRUSTSTORE_PASSWORD=changeit -set INITIAL_ADMIN_PASSWORD_FILE=.\.s - +if exist ".\.initial_admin_password" ( + set INITIAL_ADMIN_PASSWORD_FILE=.\.initial_admin_password +) -REM Check if INITIAL_ADMIN_PASSWORD_FILE and INITIAL_ADMIN_PASSWORD are empty -REM Set default password if neither is provided -if "%INITIAL_ADMIN_PASSWORD_FILE%"=="" if "%INITIAL_ADMIN_PASSWORD%"=="" ( - set INITIAL_ADMIN_PASSWORD=admin +REM Database configuration +if "%DB_HOST%"=="" (set DB_HOST=localhost) +if "%POSTGRES_PASSWORD_FILE%"=="" ( + if exist "..\.db_password" ( + for %%i in ("..\.db_password") do set POSTGRES_PASSWORD_FILE=%%~fi + ) else ( + if exist ".\.db_password" ( + for %%i in (".\.db_password") do set POSTGRES_PASSWORD_FILE=%%~fi + ) + ) ) -REM Call the next batch script to handle setting the initial admin password -CALL "%SCRIPT_DIR%set-initial-admin-password.bat" +REM Check if INITIAL_ADMIN_PASSWORD_FILE or INITIAL_ADMIN_PASSWORD are provided +REM If so, call the next batch script to handle setting the initial admin password +if not "%INITIAL_ADMIN_PASSWORD_FILE%"=="" ( + CALL "%SCRIPT_DIR%set-initial-admin-password.bat" +) else ( + if not "%INITIAL_ADMIN_PASSWORD%"=="" ( + CALL "%SCRIPT_DIR%set-initial-admin-password.bat" + ) +) REM Start the node java -Xms6g -Xmx6g -Xss256k -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError ^ -Dlogback.configurationFile=./logback.xml ^ -cp "lib/*" ^ -Djava.system.class.loader="org.sensorhub.utils.NativeClassLoader" ^ - -Djavax.net.ssl.keyStore="./osh-keystore.p12" ^ - -Djavax.net.ssl.keyStorePassword="atakatak" ^ - -Djavax.net.ssl.trustStore="%~dp0trustStore.jks" ^ - -Djavax.net.ssl.trustStorePassword="changeit" ^ com.botts.impl.security.SensorHubWrapper config.json db diff --git a/dist/scripts/standard/launch.sh b/dist/scripts/standard/launch.sh index 856b549..dd14189 100755 --- a/dist/scripts/standard/launch.sh +++ b/dist/scripts/standard/launch.sh @@ -4,23 +4,46 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) "$SCRIPT_DIR/load_trusted_certs.sh" - export KEYSTORE="./osh-keystore.p12" - export KEYSTORE_TYPE=PKCS12 - export KEYSTORE_PASSWORD="atakatak" +export KEYSTORE="./osh-keystore.p12" +export KEYSTORE_TYPE=PKCS12 - export TRUSTSTORE="./truststore.jks" +# Ephemeral CA Generation +if [ ! -f "$KEYSTORE" ]; then + echo "Generating ephemeral certificates..." + java -cp "lib/*" com.botts.impl.security.EphemeralCAUtility +fi + +if [ -f ".app_secrets" ]; then + export KEYSTORE_PASSWORD=$(head -n 1 .app_secrets) +else + export KEYSTORE_PASSWORD="atakatak" +fi + +export TRUSTSTORE="./truststore.jks" export TRUSTSTORE_TYPE=JKS export TRUSTSTORE_PASSWORD="changeit" - export INITIAL_ADMIN_PASSWORD_FILE="./.s" + if [ -f "./.initial_admin_password" ]; then + export INITIAL_ADMIN_PASSWORD_FILE="./.initial_admin_password" + fi + +# Database configuration +export DB_HOST="${DB_HOST:-localhost}" +if [ -z "$POSTGRES_PASSWORD_FILE" ]; then + # Check for password file in parent directory (standard for release) or current + if [ -f "../.db_password" ]; then + export POSTGRES_PASSWORD_FILE="$(cd .. && pwd)/.db_password" + elif [ -f "./.db_password" ]; then + export POSTGRES_PASSWORD_FILE="$(pwd)/.db_password" + fi +fi # After copying the default configuration file, also look to see if they # specified what they want the initial admin user's password to be, either # as a secret file or by providing it as an environment variable. -if [ -z "$INITIAL_ADMIN_PASSWORD_FILE" ] && [ -z "$INITIAL_ADMIN_PASSWORD" ]; then - export INITIAL_ADMIN_PASSWORD=admin +if [ ! -z "$INITIAL_ADMIN_PASSWORD_FILE" ] || [ ! -z "$INITIAL_ADMIN_PASSWORD" ]; then + "$SCRIPT_DIR/set-initial-admin-password.sh" fi -"$SCRIPT_DIR/set-initial-admin-password.sh" @@ -29,8 +52,4 @@ java -Xms6g -Xmx6g -Xss256k -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC -XX:+Hea -Dlogback.configurationFile=./logback.xml \ -cp "lib/*" \ -Djava.system.class.loader="org.sensorhub.utils.NativeClassLoader" \ - -Djavax.net.ssl.keyStore="./osh-keystore.p12" \ - -Djavax.net.ssl.keyStorePassword="atakatak" \ - -Djavax.net.ssl.trustStore="$SCRIPT_DIR/trustStore.jks" \ - -Djavax.net.ssl.trustStorePassword="changeit" \ com.botts.impl.security.SensorHubWrapper ./config.json ./db diff --git a/docs/ADDING_NEW_RADIATION_PORTAL_MONITOR.md b/docs/ADDING_NEW_RADIATION_PORTAL_MONITOR.md new file mode 100644 index 0000000..1dea20e --- /dev/null +++ b/docs/ADDING_NEW_RADIATION_PORTAL_MONITOR.md @@ -0,0 +1,45 @@ +# Adding a New Radiation Portal Monitor System + +This document provides the correct process for adding a new radiation portal monitor system using the Lane Systems sensor. + +## Step 1: Add a New Module +1. Navigate to the admin panel at [localhost:8282/sensorhub/admin](http://localhost:8282/sensorhub/admin). +2. Right-click in the **Sensors** area. +3. Click **Add New Module**. +4. Select the **Lane Systems** module. + +## Step 2: Configure Location and Manufacturer +1. Enter the required **Location Information**. +2. Go to the tab that allows you to enter the radiation portal monitor manufacturer (Rapiscan or Aspect). +3. If you select **Rapiscan**, enter the **IP address** and the **communication port**. + +## Step 3: Add Camera Systems +Add the camera system of your choice by selecting the appropriate option: **Sony**, **Axis**, or **Generic**. + +### Sony or Axis Cameras +- If a Sony or Axis camera is selected, you will be presented with options to enter the **username**, **password**, and **IP address** for the camera. +- **Sony** cameras will additionally have the ability to choose between the **MJPG** and **H.264** video streams. + +### Generic Cameras +- If a Generic camera is selected, you will have the option to enter the **username**, **password**, **IP address**, **port number**, and the **stream URL** (this consists of the information that follows the initial IP address of the camera). + +*Note: More than one camera may be added by selecting to add additional cameras.* + +## Step 4: Save Changes +**Important: The save buttons must be clicked in the specific order described below.** + +1. **Session Save:** After all information has been entered for the radiation portal monitor system, save your changes by clicking the appropriate button on the **right side** of the upper corner of the screen. + *(Note: Saving only via the button on the right side saves the changes **only during the current session** that the node is running in.)* +2. **Persistent Save:** Next, save the changes by clicking the save icon on the **left side** of the screen. + *(Note: Clicking the save icon on the upper left-hand portion of the screen actually saves the configuration into the configuration file, ensuring they will persist after the node is restarted.)* + +## Step 5: Start the Module +1. Right-click on the newly created module in the Sensors area and select **Start**. +2. The module may go through an initial initialization phase where communication is established between the node and each of the sensors. +3. After initialization, the module will start. + *(Note: Modules can also be configured to auto-start when the node is started.)* + +## Step 6: Verify in OSCAR Viewer +Once the module has been successfully added and started: +1. Switch to the Oscar Viewer Dashboard at [localhost:8282](http://localhost:8282). +2. You will often need to **refresh this page** to make the newly added sensors appear. diff --git a/include/osh-addons b/include/osh-addons index ce66c7f..a048c64 160000 --- a/include/osh-addons +++ b/include/osh-addons @@ -1 +1 @@ -Subproject commit ce66c7f8aabddbff05ed07c9bbe07fd0c436d24e +Subproject commit a048c64fc9a906fd7f27b8e0e349a5963251cf93 diff --git a/include/osh-core b/include/osh-core index 7af3a11..fbaaf87 160000 --- a/include/osh-core +++ b/include/osh-core @@ -1 +1 @@ -Subproject commit 7af3a119dde5241e19fd94ae459aac192780fc3d +Subproject commit fbaaf87d4bb1947134350ffe1507a402bfb1b15a diff --git a/include/osh-oakridge-modules b/include/osh-oakridge-modules index e3b15fc..9a344af 160000 --- a/include/osh-oakridge-modules +++ b/include/osh-oakridge-modules @@ -1 +1 @@ -Subproject commit e3b15fce02cd9eeb1d3a4a365ae6451bf03bcf17 +Subproject commit 9a344afcdbba4f8aaf27fd4a16fa9d569ee2bb45 diff --git a/patch_osh_login.diff b/patch_osh_login.diff new file mode 100644 index 0000000..05cf61e --- /dev/null +++ b/patch_osh_login.diff @@ -0,0 +1,122 @@ +<<<<<<< SEARCH + public static String getBridgedUser(HttpServletRequest req, ISecurityManager securityManager) + { + try { + // 1. First check local session for performance + javax.servlet.http.HttpSession localSession = req.getSession(false); + if (localSession != null) { + String user = (String) localSession.getAttribute("VERIFIED_USER"); + if (user != null) return user; + + // search bridge if VERIFIED_USER missing + String cid = getCleanId(localSession.getId()); + for (String entry : securityManager.get2FAVerifiedSessions()) { + if (entry.startsWith(cid + ":")) { + String found = entry.substring(cid.length() + 1); + localSession.setAttribute("VERIFIED_USER", found); + localSession.setAttribute("2FA_VERIFIED", true); + return found; + } + } + } + + // 2. Search all cookies for a bridged session + String cookieHeader = req.getHeader("Cookie"); + if (cookieHeader != null) { + for (String cookie : cookieHeader.split(";")) { + String[] parts = cookie.trim().split("=", 2); + if (parts.length == 2 && parts[0].trim().contains("JSESSIONID")) { + String cid = getCleanId(parts[1].trim()); + if (cid != null) { + for (String entry : securityManager.get2FAVerifiedSessions()) { + if (entry.startsWith(cid + ":")) { + String foundUser = entry.substring(cid.length() + 1); + // Auto-bridge current local session if it exists + if (localSession != null) { + String currentCid = getCleanId(localSession.getId()); + securityManager.get2FAVerifiedSessions().add(currentCid + ":" + foundUser); + localSession.setAttribute("2FA_VERIFIED", true); + localSession.setAttribute("VERIFIED_USER", foundUser); + } + return foundUser; + } + } + } + } + } + } + } catch (Exception e) {} + return null; + } +======= + public static String getBridgedUser(HttpServletRequest req, ISecurityManager securityManager) + { + try { + System.err.println("--- [DEBUG] getBridgedUser called for path: " + req.getRequestURI()); + System.err.println("--- [DEBUG] Remote user: " + req.getRemoteUser()); + // 1. First check local session for performance + javax.servlet.http.HttpSession localSession = req.getSession(false); + if (localSession != null) { + System.err.println("--- [DEBUG] localSession id: " + localSession.getId()); + String user = (String) localSession.getAttribute("VERIFIED_USER"); + if (user != null) { + System.err.println("--- [DEBUG] found VERIFIED_USER in localSession: " + user); + return user; + } + + // search bridge if VERIFIED_USER missing + String cid = getCleanId(localSession.getId()); + for (String entry : securityManager.get2FAVerifiedSessions()) { + if (entry.startsWith(cid + ":")) { + String found = entry.substring(cid.length() + 1); + localSession.setAttribute("VERIFIED_USER", found); + localSession.setAttribute("2FA_VERIFIED", true); + System.err.println("--- [DEBUG] found VERIFIED_USER via bridged localSession: " + found); + return found; + } + } + } else { + System.err.println("--- [DEBUG] localSession is null"); + } + + // 2. Search all cookies for a bridged session + String cookieHeader = req.getHeader("Cookie"); + System.err.println("--- [DEBUG] Cookie header: " + cookieHeader); + if (cookieHeader != null) { + for (String cookie : cookieHeader.split(";")) { + String[] parts = cookie.trim().split("=", 2); + if (parts.length == 2 && parts[0].trim().contains("JSESSIONID")) { + String cid = getCleanId(parts[1].trim()); + if (cid != null) { + for (String entry : securityManager.get2FAVerifiedSessions()) { + if (entry.startsWith(cid + ":")) { + String foundUser = entry.substring(cid.length() + 1); + // Auto-bridge current local session if it exists + if (localSession != null) { + String currentCid = getCleanId(localSession.getId()); + securityManager.get2FAVerifiedSessions().add(currentCid + ":" + foundUser); + localSession.setAttribute("2FA_VERIFIED", true); + localSession.setAttribute("VERIFIED_USER", foundUser); + } + System.err.println("--- [DEBUG] found user via cookie JSESSIONID bridge: " + foundUser); + return foundUser; + } + } + } + } + } + } + + // Check auth header directly to see if it's there + String authHeader = req.getHeader("Authorization"); + if (authHeader != null) { + System.err.println("--- [DEBUG] Authorization header is present: " + authHeader.substring(0, Math.min(10, authHeader.length())) + "..."); + } + + } catch (Exception e) { + System.err.println("--- [DEBUG] Error in getBridgedUser: " + e.getMessage()); + } + System.err.println("--- [DEBUG] getBridgedUser returning null"); + return null; + } +>>>>>>> REPLACE diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..78e0ed1 --- /dev/null +++ b/plan.md @@ -0,0 +1,13 @@ +1. **Understand the problem**: + - WebSocket authentication fails for MQTT streams in the frontend, despite HTTP requests working. + - We need to add logging to `HttpServer.java` and `OshLoginService.java` to trace the `getBridgedUser` logic and see why it returns `null` for WebSockets. + +2. **Add Logging to `OshLoginService.java`**: + - In `getBridgedUser`, log the `cookieHeader` and local session ID. + - Log if a bridged user is found or not. + +3. **Compile and build**: + - Run `./gradlew :sensorhub-core:build` to compile `osh-core`. + +4. **Instruct User**: + - Ask the user to run the build with the new logging and capture the output. diff --git a/restore.bat b/restore.bat new file mode 100644 index 0000000..51eb88e --- /dev/null +++ b/restore.bat @@ -0,0 +1,34 @@ +@echo off +setlocal enabledelayedexpansion + +if "%DB_HOST%"=="" (set DB_HOST=localhost) +set DB_NAME=gis +set DB_USER=postgres + +if "%POSTGRES_PASSWORD_FILE%"=="" ( + echo Error: POSTGRES_PASSWORD_FILE environment variable is not set. + exit /b 1 +) + +if not exist "%POSTGRES_PASSWORD_FILE%" ( + echo Error: Password file %POSTGRES_PASSWORD_FILE% does not exist. + exit /b 1 +) + +if "%~1"=="" ( + echo Usage: %0 ^ + exit /b 1 +) + +set BACKUP_FILE=%~1 +set /p PGPASSWORD=<"%POSTGRES_PASSWORD_FILE%" + +echo Restoring database %DB_NAME% to %DB_HOST% from %BACKUP_FILE%... +pg_restore -h %DB_HOST% -U %DB_USER% -d %DB_NAME% -v "%BACKUP_FILE%" + +if %errorlevel% equ 0 ( + echo Restore completed successfully. +) else ( + echo Restore failed. + exit /b 1 +) diff --git a/restore.sh b/restore.sh new file mode 100755 index 0000000..5946c96 --- /dev/null +++ b/restore.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +DB_HOST="${DB_HOST:-localhost}" +DB_NAME="gis" +DB_USER="postgres" + +if [ -z "$POSTGRES_PASSWORD_FILE" ]; then + echo "Error: POSTGRES_PASSWORD_FILE environment variable is not set." + exit 1 +fi + +if [ ! -f "$POSTGRES_PASSWORD_FILE" ]; then + echo "Error: Password file $POSTGRES_PASSWORD_FILE does not exist." + exit 1 +fi + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +BACKUP_FILE="$1" +export PGPASSWORD=$(cat "$POSTGRES_PASSWORD_FILE") + +echo "Restoring database $DB_NAME to $DB_HOST from $BACKUP_FILE..." +pg_restore -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -v "$BACKUP_FILE" + +if [ $? -eq 0 ]; then + echo "Restore completed successfully." +else + echo "Restore failed." + exit 1 +fi diff --git a/security-utils/build.gradle b/security-utils/build.gradle index 98b9df7..c8614a9 100644 --- a/security-utils/build.gradle +++ b/security-utils/build.gradle @@ -4,6 +4,8 @@ version = '1.0.0-SNAPSHOT' dependencies { implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion + implementation 'org.bouncycastle:bcpkix-jdk18on:1.77' + implementation 'org.bouncycastle:bcprov-jdk18on:1.77' } test { diff --git a/security-utils/src/main/java/com/botts/impl/security/EphemeralCAUtility.java b/security-utils/src/main/java/com/botts/impl/security/EphemeralCAUtility.java new file mode 100644 index 0000000..4d9bf9d --- /dev/null +++ b/security-utils/src/main/java/com/botts/impl/security/EphemeralCAUtility.java @@ -0,0 +1,155 @@ +package com.botts.impl.security; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.EnumSet; +import java.util.Set; +import java.util.Base64; +import javax.security.auth.x500.X500Principal; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; + +public class EphemeralCAUtility { + + public static void main(String[] args) throws Exception { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + + String keystorePath = "osh-keystore.p12"; + String secretsPath = ".app_secrets"; + String rootCaExportPath = "root-ca.crt"; + + if (new File(keystorePath).exists()) { + System.out.println("Keystore already exists. Skipping generation."); + return; + } + + // 1. Generate Keystore Password + String password = generateRandomPassword(32); + saveSecret(secretsPath, password); + + // 2. Generate Root CA (In-memory private key) + KeyPair rootKeyPair = generateKeyPair(); + X509Certificate rootCert = generateCertificate("CN=OSCAR Root CA", "CN=OSCAR Root CA", rootKeyPair.getPublic(), rootKeyPair.getPrivate(), true); + + // 3. Generate Leaf Certificate signed by Root CA + KeyPair leafKeyPair = generateKeyPair(); + X509Certificate leafCert = generateCertificate("CN=localhost", "CN=OSCAR Root CA", leafKeyPair.getPublic(), rootKeyPair.getPrivate(), false); + + // 4. Save Leaf to Keystore + saveToKeystore(keystorePath, password, "jetty", leafKeyPair.getPrivate(), new Certificate[]{leafCert, rootCert}); + + // 5. Export Public Root CA + exportCertificate(rootCaExportPath, rootCert); + + System.out.println("Ephemeral CA and Leaf Certificate generated successfully."); + } + + private static String generateRandomPassword(int length) { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[length]; + random.nextBytes(bytes); + return Base64.getEncoder().encodeToString(bytes); + } + + private static void saveSecret(String path, String secret) throws IOException { + File file = new File(path); + try (FileWriter writer = new FileWriter(file)) { + writer.write(secret); + } + lockdownFile(file); + } + + private static void lockdownFile(File file) { + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + try { + java.nio.file.Path path = file.toPath(); + java.nio.file.attribute.AclFileAttributeView view = Files.getFileAttributeView(path, java.nio.file.attribute.AclFileAttributeView.class); + java.nio.file.attribute.UserPrincipal owner = Files.getOwner(path); + java.nio.file.attribute.AclEntry entry = java.nio.file.attribute.AclEntry.newBuilder() + .setType(java.nio.file.attribute.AclEntryType.ALLOW) + .setPrincipal(owner) + .setPermissions(java.nio.file.attribute.AclEntryPermission.READ_DATA, + java.nio.file.attribute.AclEntryPermission.WRITE_DATA, + java.nio.file.attribute.AclEntryPermission.APPEND_DATA, + java.nio.file.attribute.AclEntryPermission.READ_NAMED_ATTRS, + java.nio.file.attribute.AclEntryPermission.WRITE_NAMED_ATTRS, + java.nio.file.attribute.AclEntryPermission.READ_ATTRIBUTES, + java.nio.file.attribute.AclEntryPermission.WRITE_ATTRIBUTES, + java.nio.file.attribute.AclEntryPermission.READ_ACL, + java.nio.file.attribute.AclEntryPermission.WRITE_ACL, + java.nio.file.attribute.AclEntryPermission.WRITE_OWNER, + java.nio.file.attribute.AclEntryPermission.SYNCHRONIZE) + .build(); + view.setAcl(java.util.Collections.singletonList(entry)); + } catch (IOException e) { + System.err.println("Failed to set Windows ACLs: " + e.getMessage()); + } + } else { + try { + Set perms = EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(file.toPath(), perms); + } catch (Exception e) { + System.err.println("Failed to set POSIX permissions: " + e.getMessage()); + } + } + } + + private static KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + return keyGen.generateKeyPair(); + } + + private static X509Certificate generateCertificate(String dn, String issuerDn, PublicKey publicKey, PrivateKey signerPrivateKey, boolean isCa) throws Exception { + X500Name subjectName = new X500Name(dn); + X500Name issuerName = new X500Name(issuerDn); + BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); + Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24); + Date notAfter = new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 365); + + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + issuerName, serialNumber, notBefore, notAfter, subjectName, publicKey); + + if (isCa) { + certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + } + + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(signerPrivateKey); + X509CertificateHolder certHolder = certBuilder.build(signer); + return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder); + } + + private static void saveToKeystore(String path, String password, String alias, PrivateKey privateKey, Certificate[] chain) throws Exception { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + ks.setKeyEntry(alias, privateKey, password.toCharArray(), chain); + try (FileOutputStream fos = new FileOutputStream(path)) { + ks.store(fos, password.toCharArray()); + } + lockdownFile(new File(path)); + } + + private static void exportCertificate(String path, X509Certificate cert) throws Exception { + try (FileOutputStream fos = new FileOutputStream(path)) { + fos.write(cert.getEncoded()); + } + } +} diff --git a/security-utils/src/main/java/com/botts/impl/security/PBKDF2Credential.java b/security-utils/src/main/java/com/botts/impl/security/PBKDF2Credential.java index 09a7f95..509c795 100644 --- a/security-utils/src/main/java/com/botts/impl/security/PBKDF2Credential.java +++ b/security-utils/src/main/java/com/botts/impl/security/PBKDF2Credential.java @@ -1,142 +1,150 @@ -package com.botts.impl.security; - -import java.security.GeneralSecurityException; -import java.security.SecureRandom; -import java.security.spec.KeySpec; -import java.util.Arrays; -import java.util.Base64; - -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; - -import org.eclipse.jetty.util.security.Credential; -import org.eclipse.jetty.util.security.Password; - -/** - * Represents a password that has been hashed using PBKDF2 and the SHA1 HMAC. - */ -public class PBKDF2Credential extends Credential { - private static final long serialVersionUID = 1L; - - /** - * Log base 2 of the number of hashing iterations to use, by default. This can be used to increase the difficulty - * of brute-force attacks by increasing the calculations necessary for each password check. - */ - public static final int DEFAULT_STRENGTH = 16; - - /** - * How many bits are calculated when the password is hashed. This is fixed by the choice of algorithm. - */ - public static final int HASH_BITS = 128; - - /** - * How many random bytes are generated for salt. This is also fixed by the choice of algorithm. - */ - public static final int SALT_LENGTH = 16; - - /** - * What character is used to separate the components of the encoded password when it is stringified for saving in a - * config file or database. - */ - public static final char SEPARATOR = ':'; - - /** - * Secret key algorithm to use. This must be known to the JSSE implementation at runtime. - */ - public static final String ALGORITHM = "PBKDF2WithHmacSHA1"; - - /** - * Prefix to use when the password is stringified. Lets Jetty identify this credential provider. - */ - public static final String PREFIX = ALGORITHM + SEPARATOR; - - private final String stringifiedCredential; - private final byte[] salt; - private final byte[] hash; - private final int strength; - - private PBKDF2Credential(String stringifiedCredential, byte[] salt, byte[] hash, int strength) { - this.stringifiedCredential = stringifiedCredential; - this.salt = salt; - this.hash = hash; - this.strength = strength; - } - - public static PBKDF2Credential fromEncoded(String stringifiedCredential) { - String strengthSaltHashString = stringifiedCredential.substring(PREFIX.length()); - int separatorIndex; - - separatorIndex = strengthSaltHashString.indexOf(SEPARATOR); - String strengthString = strengthSaltHashString.substring(0, separatorIndex); - String saltHashString = strengthSaltHashString.substring(separatorIndex + 1); - separatorIndex = saltHashString.indexOf(SEPARATOR); - String saltString = saltHashString.substring(0, separatorIndex); - String hashString = saltHashString.substring(separatorIndex + 1); - - Base64.Decoder base64Decoder = Base64.getDecoder(); - byte[] salt = base64Decoder.decode(saltString); - byte[] hash = base64Decoder.decode(hashString); - int strength = Integer.parseInt(strengthString); - return new PBKDF2Credential(stringifiedCredential, salt, hash, strength); - } - - public static PBKDF2Credential fromPassword(String password, int strength) throws GeneralSecurityException { - SecureRandom random = new SecureRandom(); - byte[] salt = new byte[16]; - random.nextBytes(salt); - - KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, getIterationsFromStrength(strength), HASH_BITS); - SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM); - - byte[] hash = secretKeyFactory.generateSecret(keySpec).getEncoded(); - - Base64.Encoder base64Encoder = Base64.getEncoder(); - - String stringifiedCredential = PREFIX + strength + SEPARATOR + base64Encoder.encodeToString(salt) + - SEPARATOR + base64Encoder.encodeToString(hash); - - return new PBKDF2Credential(stringifiedCredential, salt, hash, strength); - } - - private boolean check(String password) { - try { - KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, getIterationsFromStrength(strength), HASH_BITS); - SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM); - byte[] testHash = secretKeyFactory.generateSecret(keySpec).getEncoded(); - return Arrays.equals(hash, testHash); - } catch (GeneralSecurityException gse) { - throw new RuntimeException("Unable to check password", gse); - } - } - - private static int getIterationsFromStrength(int strength) { - return 1 << strength; - } - - @Override - public boolean check(Object credentials) { - if (credentials == null) { - return false; - } - if (credentials instanceof String) { - String password = (String) credentials; - return check(password); - } - if (credentials instanceof Password) { - String password = ((Password) credentials).toString(); - return check(password); - } - if (credentials instanceof char[]) { - String password = new String((char[]) credentials); - return check(password); - } - // We don't know how to validate against any other types of credential - // input, so we return false in those cases. - return false; - } - - @Override - public String toString() { - return stringifiedCredential; - } -} +package com.botts.impl.security; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.spec.KeySpec; +import java.util.Arrays; +import java.util.Base64; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +import org.eclipse.jetty.util.security.Credential; +import org.eclipse.jetty.util.security.Password; + +/** + * Represents a password that has been hashed using PBKDF2 and the SHA1 HMAC. + */ +public class PBKDF2Credential extends Credential { + private static final long serialVersionUID = 1L; + + /** + * Log base 2 of the number of hashing iterations to use, by default. This can be used to increase the difficulty + * of brute-force attacks by increasing the calculations necessary for each password check. + */ + public static final int DEFAULT_STRENGTH = 16; + + /** + * How many bits are calculated when the password is hashed. This is fixed by the choice of algorithm. + */ + public static final int HASH_BITS = 128; + + /** + * How many random bytes are generated for salt. This is also fixed by the choice of algorithm. + */ + public static final int SALT_LENGTH = 16; + + /** + * What character is used to separate the components of the encoded password when it is stringified for saving in a + * config file or database. + */ + public static final char SEPARATOR = ':'; + + /** + * Secret key algorithm to use. This must be known to the JSSE implementation at runtime. + */ + public static final String ALGORITHM = "PBKDF2WithHmacSHA1"; + + /** + * Prefix to use when the password is stringified. Lets Jetty identify this credential provider. + */ + public static final String PREFIX = ALGORITHM + SEPARATOR; + + private final String stringifiedCredential; + private final byte[] salt; + private final byte[] hash; + private final int strength; + + private PBKDF2Credential(String stringifiedCredential, byte[] salt, byte[] hash, int strength) { + this.stringifiedCredential = stringifiedCredential; + this.salt = salt; + this.hash = hash; + this.strength = strength; + } + + public static PBKDF2Credential fromEncoded(String stringifiedCredential) { + String strengthSaltHashString = stringifiedCredential.substring(PREFIX.length()); + int separatorIndex; + + separatorIndex = strengthSaltHashString.indexOf(SEPARATOR); + String strengthString = strengthSaltHashString.substring(0, separatorIndex); + String saltHashString = strengthSaltHashString.substring(separatorIndex + 1); + separatorIndex = saltHashString.indexOf(SEPARATOR); + String saltString = saltHashString.substring(0, separatorIndex); + String hashString = saltHashString.substring(separatorIndex + 1); + + Base64.Decoder base64Decoder = Base64.getDecoder(); + byte[] salt = base64Decoder.decode(saltString); + byte[] hash = base64Decoder.decode(hashString); + int strength = Integer.parseInt(strengthString); + return new PBKDF2Credential(stringifiedCredential, salt, hash, strength); + } + + public static PBKDF2Credential fromPassword(String password) { + try { + return fromPassword(password, DEFAULT_STRENGTH); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + public static PBKDF2Credential fromPassword(String password, int strength) throws GeneralSecurityException { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[16]; + random.nextBytes(salt); + + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, getIterationsFromStrength(strength), HASH_BITS); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM); + + byte[] hash = secretKeyFactory.generateSecret(keySpec).getEncoded(); + + Base64.Encoder base64Encoder = Base64.getEncoder(); + + String stringifiedCredential = PREFIX + strength + SEPARATOR + base64Encoder.encodeToString(salt) + + SEPARATOR + base64Encoder.encodeToString(hash); + + return new PBKDF2Credential(stringifiedCredential, salt, hash, strength); + } + + private boolean check(String password) { + try { + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, getIterationsFromStrength(strength), HASH_BITS); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM); + byte[] testHash = secretKeyFactory.generateSecret(keySpec).getEncoded(); + return Arrays.equals(hash, testHash); + } catch (GeneralSecurityException gse) { + throw new RuntimeException("Unable to check password", gse); + } + } + + private static int getIterationsFromStrength(int strength) { + return 1 << strength; + } + + @Override + public boolean check(Object credentials) { + if (credentials == null) { + return false; + } + if (credentials instanceof String) { + String password = (String) credentials; + return check(password); + } + if (credentials instanceof Password) { + String password = ((Password) credentials).toString(); + return check(password); + } + if (credentials instanceof char[]) { + String password = new String((char[]) credentials); + return check(password); + } + // We don't know how to validate against any other types of credential + // input, so we return false in those cases. + return false; + } + + @Override + public String toString() { + return stringifiedCredential; + } +} diff --git a/security-utils/src/main/java/com/botts/impl/security/PBKDF2CredentialProvider.java b/security-utils/src/main/java/com/botts/impl/security/PBKDF2CredentialProvider.java index a900c4e..33c3470 100644 --- a/security-utils/src/main/java/com/botts/impl/security/PBKDF2CredentialProvider.java +++ b/security-utils/src/main/java/com/botts/impl/security/PBKDF2CredentialProvider.java @@ -1,51 +1,55 @@ -package com.botts.impl.security; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.security.GeneralSecurityException; - -import org.eclipse.jetty.util.security.Credential; -import org.eclipse.jetty.util.security.CredentialProvider; - -public class PBKDF2CredentialProvider implements CredentialProvider { - private int strength = PBKDF2Credential.DEFAULT_STRENGTH; - - public PBKDF2CredentialProvider() { - } - - public int getStrength() { - return strength; - } - - public void setStrength(int strength) { - this.strength = strength; - } - - @Override - public Credential getCredential(String credential) { - return PBKDF2Credential.fromEncoded(credential); - } - - @Override - public String getPrefix() { - return PBKDF2Credential.PREFIX; - } - - public static void main(String[] args) throws IOException, GeneralSecurityException { - String password; - int strength = PBKDF2Credential.DEFAULT_STRENGTH; - if (args.length > 0) { - strength = Integer.parseInt(args[0]); - } - if (args.length > 1) { - password = args[1]; - } else { - try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in))) { - password = bufferedReader.readLine(); - } - } - PBKDF2Credential credential = PBKDF2Credential.fromPassword(password, strength); - System.out.println(credential.toString()); - } -} +package com.botts.impl.security; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.GeneralSecurityException; + +import org.eclipse.jetty.util.security.Credential; +import org.eclipse.jetty.util.security.CredentialProvider; + +public class PBKDF2CredentialProvider implements CredentialProvider { + private int strength = PBKDF2Credential.DEFAULT_STRENGTH; + + public PBKDF2CredentialProvider() { + } + + public int getStrength() { + return strength; + } + + public void setStrength(int strength) { + this.strength = strength; + } + + @Override + public Credential getCredential(String credential) { + return PBKDF2Credential.fromEncoded(credential); + } + + @Override + public String getPrefix() { + return PBKDF2Credential.PREFIX; + } + + public static String encode(String password) { + return PBKDF2Credential.fromPassword(password).toString(); + } + + public static void main(String[] args) throws IOException, GeneralSecurityException { + String password; + int strength = PBKDF2Credential.DEFAULT_STRENGTH; + if (args.length > 0) { + strength = Integer.parseInt(args[0]); + } + if (args.length > 1) { + password = args[1]; + } else { + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in))) { + password = bufferedReader.readLine(); + } + } + PBKDF2Credential credential = PBKDF2Credential.fromPassword(password, strength); + System.out.println(credential.toString()); + } +} diff --git a/security-utils/src/main/java/com/botts/impl/security/SensorHubWrapper.java b/security-utils/src/main/java/com/botts/impl/security/SensorHubWrapper.java index 90fa66d..56d42fb 100644 --- a/security-utils/src/main/java/com/botts/impl/security/SensorHubWrapper.java +++ b/security-utils/src/main/java/com/botts/impl/security/SensorHubWrapper.java @@ -1,194 +1,203 @@ -package com.botts.impl.security; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - -import org.sensorhub.impl.SensorHub; - -/** - * Simple wrapper around org.sensorhub.impl.SensorHub that sets TLS-related system properties programmatically so that - * passwords are not shown in the process's command line (in "-Djavax.net.ssl.keyStorePassword" for example). - */ -public class SensorHubWrapper { - /** - * Name of the environment variable that specifies the path to a keystore that will be used for the - * "javax.net.ssl.keyStore" system property. - */ - public static final String KEYSTORE = "KEYSTORE"; - - /** - * Name of the environment variable that specifies the type (e.g. "jks" or "pkcs12") of the file named by the - * KEYSTORE environment variable. This is used for the "javax.net.ssl.keyStoreType" system property. - */ - public static final String KEYSTORE_TYPE = "KEYSTORE_TYPE"; - - /** - * Name of the environment variable that specifies the password for the keystore. Users should prefer to use the - * KEYSTORE_PASSWORD_FILE environment variable instead, though. - */ - public static final String KEYSTORE_PASSWORD = "KEYSTORE_PASSWORD"; - - /** - * Name of the environment variable that specifies the path to a certificate store that will be used for the - * "javax.net.ssl.trustStore" system property. - */ - public static final String TRUSTSTORE = "TRUSTSTORE"; - - /** - * Name of the environment variable that specifies the type (e.g. "jks" or "pkcs12") of the file named by the - * TRUSTSTORE environment variable. This is used for the "javax.net.ssl.trustStoreType" system property. - */ - public static final String TRUSTSTORE_TYPE = "TRUSTSTORE_TYPE"; - - /** - * Name of the environment variable that specifies the password for the trsut store. Users should prefer to use the - * TRUSTSTORE_PASSWORD_FILE environment variable instead. - */ - public static final String TRUSTSTORE_PASSWORD = "TRUSTSTORE_PASSWORD"; - - /** - * Suffix to add to the names of the password-related environment variables that will instruct us to get it from - * the named file, rather than from the value of the environment variable itself. - */ - public static final String FILE_SUFFIX = "_FILE"; - - /** - * Name of the environment variable that, if set to a non-empty value, will cause this class to emit some - * information about where it loaded certificates from. - */ - public static final String SHOW_CMD = "SHOW_CMD"; - - public static void main(String[] args) throws IOException { - String showCmdEnv = System.getenv(SHOW_CMD); - boolean debug = nonBlank(showCmdEnv); - - // We're assuming that the startup script will have set values for these things so that we don't have to check - // for empty/non-set values. - String keyStoreEnv = System.getenv(KEYSTORE); - String keyStoreTypeEnv = System.getenv(KEYSTORE_TYPE); - PasswordValue keyStorePassword = getPasswordValue(KEYSTORE_PASSWORD, "changeit"); - - System.setProperty("javax.net.ssl.keyStore", keyStoreEnv); - System.setProperty("javax.net.ssl.keyStoreType", keyStoreTypeEnv); - System.setProperty("javax.net.ssl.keyStorePassword", keyStorePassword.getValue()); - - String trustStoreEnv = System.getenv(TRUSTSTORE); - String trustStoreTypeEnv = System.getenv(TRUSTSTORE_TYPE); - PasswordValue trustStorePassword = getPasswordValue(TRUSTSTORE_PASSWORD, "changeit"); - - System.setProperty("javax.net.ssl.trustStore", trustStoreEnv); - System.setProperty("javax.net.ssl.trustStoreType", trustStoreTypeEnv); - System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword.getValue()); - - if (debug) { - System.out.println("Key store: " + keyStoreEnv); - System.out.println("Key store type: " + keyStoreTypeEnv); - System.out.println("Key store password: " + keyStorePassword.getDescription()); - - System.out.println("Trust store: " + trustStoreEnv); - System.out.println("Trust store type: " + trustStoreTypeEnv); - System.out.println("Trust store password: " + trustStorePassword.getDescription()); - } - - SensorHub.main(args); - } - - /** - * Utility method for getting passwords from environment variables. - * - * We're assuming that passwords will be provided in one of two ways: (1) by specifying a "secret file" in an - * environment variable named "XXX_FILE", whose content is the password, or (2) by specifying the password - * directly in an environment variable named just "XXX" (without the "_FILE" prefix). - * - * This method here checks the "_FILE" version first, and if it's present, returns the content of the file as a - * String. Otherwise it will look for plain "XXX" and return the value of that environment variable, if present. - * And if neither is present, will return the default value given as the second parameter. - */ - private static PasswordValue getPasswordValue(String envVarName, String defaultValue) throws IOException { - String fileEnvVarName = envVarName + FILE_SUFFIX; - - String filename = System.getenv(fileEnvVarName); - if (nonBlank(filename)) { - String value = firstLineOfFile(filename); - return new PasswordValue(value, fileEnvVarName, filename, PasswordSpecifier.FILE_ENVIRONMENT_VARIABLE); - } else { - String value = System.getenv(envVarName); - if (nonBlank(value)) { - return new PasswordValue(value, envVarName, null, PasswordSpecifier.ENVIRONMENT_VARIABLE); - } else { - return new PasswordValue(value, null, null, PasswordSpecifier.DEFAULT_VALUE); - } - } - } - - /** - * Reads the first line of a file and returns it as a String. Assumes UTF-8 encoding in the file. Does not include - * the line terminator in the return value. - */ - private static String firstLineOfFile(String path) throws IOException { - try (FileInputStream fileIn = new FileInputStream(path); - InputStreamReader fileReader = new InputStreamReader(fileIn, StandardCharsets.UTF_8); - BufferedReader bufferedReader = new BufferedReader(fileReader)) { - return bufferedReader.readLine(); - } - } - - /** - * Returns true if the given string is non-null and has length greater than zero. Returns false otherwise. - */ - private static boolean nonBlank(String s) { - return (s != null) && (s.length() > 0); - } - - public enum PasswordSpecifier { - ENVIRONMENT_VARIABLE, - FILE_ENVIRONMENT_VARIABLE, - DEFAULT_VALUE - } - - public static class PasswordValue { - private final String value; - private final String envVarName; - private final String filename; - private final PasswordSpecifier how; - - public PasswordValue(String value, String envVarName, String filename, PasswordSpecifier how) { - this.value = value; - this.envVarName = envVarName; - this.filename = filename; - this.how = how; - } - - public String getValue() { - return value; - } - - public String getEnvVarName() { - return envVarName; - } - - public String getFilename() { - return filename; - } - - public PasswordSpecifier getHow() { - return how; - } - - public String getDescription() { - switch (how) { - case ENVIRONMENT_VARIABLE: - return "Retrieved from environment variable \"" + envVarName + "\""; - case FILE_ENVIRONMENT_VARIABLE: - return "Retrieved from file \"" + filename + "\" (specified in environment variable \"" + envVarName + "\")"; - case DEFAULT_VALUE: - return "Using default value"; - default: - return "Unknown"; - } - } - } -} +package com.botts.impl.security; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import org.sensorhub.impl.SensorHub; + +/** + * Simple wrapper around org.sensorhub.impl.SensorHub that sets TLS-related system properties programmatically so that + * passwords are not shown in the process's command line (in "-Djavax.net.ssl.keyStorePassword" for example). + */ +public class SensorHubWrapper { + /** + * Name of the environment variable that specifies the path to a keystore that will be used for the + * "javax.net.ssl.keyStore" system property. + */ + public static final String KEYSTORE = "KEYSTORE"; + + /** + * Name of the environment variable that specifies the type (e.g. "jks" or "pkcs12") of the file named by the + * KEYSTORE environment variable. This is used for the "javax.net.ssl.keyStoreType" system property. + */ + public static final String KEYSTORE_TYPE = "KEYSTORE_TYPE"; + + /** + * Name of the environment variable that specifies the password for the keystore. Users should prefer to use the + * KEYSTORE_PASSWORD_FILE environment variable instead, though. + */ + public static final String KEYSTORE_PASSWORD = "KEYSTORE_PASSWORD"; + + /** + * Name of the environment variable that specifies the path to a certificate store that will be used for the + * "javax.net.ssl.trustStore" system property. + */ + public static final String TRUSTSTORE = "TRUSTSTORE"; + + /** + * Name of the environment variable that specifies the type (e.g. "jks" or "pkcs12") of the file named by the + * TRUSTSTORE environment variable. This is used for the "javax.net.ssl.trustStoreType" system property. + */ + public static final String TRUSTSTORE_TYPE = "TRUSTSTORE_TYPE"; + + /** + * Name of the environment variable that specifies the password for the trsut store. Users should prefer to use the + * TRUSTSTORE_PASSWORD_FILE environment variable instead. + */ + public static final String TRUSTSTORE_PASSWORD = "TRUSTSTORE_PASSWORD"; + + /** + * Suffix to add to the names of the password-related environment variables that will instruct us to get it from + * the named file, rather than from the value of the environment variable itself. + */ + public static final String FILE_SUFFIX = "_FILE"; + + /** + * Name of the environment variable that, if set to a non-empty value, will cause this class to emit some + * information about where it loaded certificates from. + */ + public static final String SHOW_CMD = "SHOW_CMD"; + + public static void main(String[] args) throws IOException { + String showCmdEnv = System.getenv(SHOW_CMD); + boolean debug = nonBlank(showCmdEnv); + + // We're assuming that the startup script will have set values for these things so that we don't have to check + // for empty/non-set values. + String keyStoreEnv = System.getenv(KEYSTORE); + String keyStoreTypeEnv = System.getenv(KEYSTORE_TYPE); + + PasswordValue keyStorePassword; + File appSecrets = new File(".app_secrets"); + if (appSecrets.exists()) { + String val = firstLineOfFile(appSecrets.getAbsolutePath()); + keyStorePassword = new PasswordValue(val, "KEYSTORE_PASSWORD_FILE", appSecrets.getAbsolutePath(), PasswordSpecifier.FILE_ENVIRONMENT_VARIABLE); + } else { + keyStorePassword = getPasswordValue(KEYSTORE_PASSWORD, "changeit"); + } + + System.setProperty("javax.net.ssl.keyStore", keyStoreEnv); + System.setProperty("javax.net.ssl.keyStoreType", keyStoreTypeEnv); + System.setProperty("javax.net.ssl.keyStorePassword", keyStorePassword.getValue()); + + String trustStoreEnv = System.getenv(TRUSTSTORE); + String trustStoreTypeEnv = System.getenv(TRUSTSTORE_TYPE); + PasswordValue trustStorePassword = getPasswordValue(TRUSTSTORE_PASSWORD, "changeit"); + + System.setProperty("javax.net.ssl.trustStore", trustStoreEnv); + System.setProperty("javax.net.ssl.trustStoreType", trustStoreTypeEnv); + System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword.getValue()); + + if (debug) { + System.out.println("Key store: " + keyStoreEnv); + System.out.println("Key store type: " + keyStoreTypeEnv); + System.out.println("Key store password: " + keyStorePassword.getDescription()); + + System.out.println("Trust store: " + trustStoreEnv); + System.out.println("Trust store type: " + trustStoreTypeEnv); + System.out.println("Trust store password: " + trustStorePassword.getDescription()); + } + + SensorHub.main(args); + } + + /** + * Utility method for getting passwords from environment variables. + * + * We're assuming that passwords will be provided in one of two ways: (1) by specifying a "secret file" in an + * environment variable named "XXX_FILE", whose content is the password, or (2) by specifying the password + * directly in an environment variable named just "XXX" (without the "_FILE" prefix). + * + * This method here checks the "_FILE" version first, and if it's present, returns the content of the file as a + * String. Otherwise it will look for plain "XXX" and return the value of that environment variable, if present. + * And if neither is present, will return the default value given as the second parameter. + */ + private static PasswordValue getPasswordValue(String envVarName, String defaultValue) throws IOException { + String fileEnvVarName = envVarName + FILE_SUFFIX; + + String filename = System.getenv(fileEnvVarName); + if (nonBlank(filename)) { + String value = firstLineOfFile(filename); + return new PasswordValue(value, fileEnvVarName, filename, PasswordSpecifier.FILE_ENVIRONMENT_VARIABLE); + } else { + String value = System.getenv(envVarName); + if (nonBlank(value)) { + return new PasswordValue(value, envVarName, null, PasswordSpecifier.ENVIRONMENT_VARIABLE); + } else { + return new PasswordValue(value, null, null, PasswordSpecifier.DEFAULT_VALUE); + } + } + } + + /** + * Reads the first line of a file and returns it as a String. Assumes UTF-8 encoding in the file. Does not include + * the line terminator in the return value. + */ + private static String firstLineOfFile(String path) throws IOException { + try (FileInputStream fileIn = new FileInputStream(path); + InputStreamReader fileReader = new InputStreamReader(fileIn, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(fileReader)) { + return bufferedReader.readLine(); + } + } + + /** + * Returns true if the given string is non-null and has length greater than zero. Returns false otherwise. + */ + private static boolean nonBlank(String s) { + return (s != null) && (s.length() > 0); + } + + public enum PasswordSpecifier { + ENVIRONMENT_VARIABLE, + FILE_ENVIRONMENT_VARIABLE, + DEFAULT_VALUE + } + + public static class PasswordValue { + private final String value; + private final String envVarName; + private final String filename; + private final PasswordSpecifier how; + + public PasswordValue(String value, String envVarName, String filename, PasswordSpecifier how) { + this.value = value; + this.envVarName = envVarName; + this.filename = filename; + this.how = how; + } + + public String getValue() { + return value; + } + + public String getEnvVarName() { + return envVarName; + } + + public String getFilename() { + return filename; + } + + public PasswordSpecifier getHow() { + return how; + } + + public String getDescription() { + switch (how) { + case ENVIRONMENT_VARIABLE: + return "Retrieved from environment variable \"" + envVarName + "\""; + case FILE_ENVIRONMENT_VARIABLE: + return "Retrieved from file \"" + filename + "\" (specified in environment variable \"" + envVarName + "\")"; + case DEFAULT_VALUE: + return "Using default value"; + default: + return "Unknown"; + } + } + } +} diff --git a/tools/sensorhub-test/src/main/resources/config.json b/tools/sensorhub-test/src/main/resources/config.json index a6fc3df..ebb0b22 100644 --- a/tools/sensorhub-test/src/main/resources/config.json +++ b/tools/sensorhub-test/src/main/resources/config.json @@ -19,19 +19,6 @@ { "objClass": "org.sensorhub.impl.security.BasicSecurityRealmConfig", "users": [ - { - "objClass": "org.sensorhub.impl.security.BasicSecurityRealmConfig$UserConfig", - "id": "admin", - "name": "Administrator", - "password": "oscar", - "roles": [ - "admin" - ], - "allow": [ - "fileserver[af72442c-1ce6-4baa-a126-ed41dda26910]" - ], - "deny": [] - }, { "objClass": "org.sensorhub.impl.security.BasicSecurityRealmConfig$UserConfig", "id": "anonymous", @@ -170,7 +157,7 @@ "url": "localhost:5432", "dbName": "gis", "login": "postgres", - "password": "postgres", + "password": "", "idProviderType": "SEQUENTIAL", "autoCommitPeriod": 10, "useBatch": false, diff --git a/web/oscar-viewer b/web/oscar-viewer index 5cbddb7..33f1427 160000 --- a/web/oscar-viewer +++ b/web/oscar-viewer @@ -1 +1 @@ -Subproject commit 5cbddb7dec77fbea96cfb3c6e8e9a0fe14df2310 +Subproject commit 33f1427c3fdb627b97d34a3d74446807450302f4