From 0c363915d650f707054d81d9097432914722a484 Mon Sep 17 00:00:00 2001 From: omar goher Date: Wed, 13 May 2026 02:21:35 +0300 Subject: [PATCH] [#50] migrate from cookie-based auth to Keycloak JWT (backend + frontend + tests + docs) --- .gitignore | 6 +- AUTHORS | 1 + README.md | 13 +- contrib/keycloak/.env-keycloak.example | 30 + contrib/keycloak/README.md | 156 +++++ contrib/keycloak/create-user.sh | 70 ++ contrib/keycloak/generate_realm.py | 59 ++ contrib/keycloak/keycloak-realm.template.json | 128 ++++ contrib/keycloak/login.sh | 25 + doc/BUILDING.md | 3 +- doc/DEVELOPERS.md | 14 +- doc/manual/en/22-security.md | 64 +- doc/manual/en/25-password-reset.md | 17 +- doc/manual/en/97-acknowledgement.md | 1 + doc/manual/en/images/keycloak-login.png | Bin 0 -> 234036 bytes .../en/images/keycloak-password-update.png | Bin 0 -> 236238 bytes docker-compose.yml | 19 + pom.xml | 9 + .../java/ai/mnemosyne_systems/model/User.java | 5 +- .../resource/AdminUserApiResource.java | 20 +- .../resource/AppSessionResource.java | 15 +- .../resource/ArticleApiResource.java | 38 +- .../resource/ArticleResource.java | 67 +- .../resource/AttachmentApiResource.java | 17 +- .../resource/AttachmentResource.java | 24 +- .../resource/AuthResource.java | 1 + .../resource/CategoryApiResource.java | 26 +- .../resource/CategoryResource.java | 47 +- .../resource/CompanyApiResource.java | 27 +- .../resource/CompanyResource.java | 59 +- .../resource/EntitlementApiResource.java | 22 +- .../resource/EntitlementResource.java | 52 +- .../resource/HomeResource.java | 17 +- .../resource/LevelApiResource.java | 26 +- .../resource/LevelResource.java | 59 +- .../resource/LogoutResource.java | 2 + .../resource/OwnerApiResource.java | 18 +- .../resource/OwnerResource.java | 26 +- .../resource/ProfileApiResource.java | 30 +- .../resource/ProfileResource.java | 33 +- .../resource/ReportApiResource.java | 29 +- .../resource/ReportResource.java | 61 +- .../resource/RssResource.java | 41 +- .../SuperuserDirectoryApiResource.java | 54 +- .../resource/SuperuserResource.java | 96 ++- .../resource/SuperuserTicketApiResource.java | 30 +- .../resource/SupportResource.java | 108 ++- .../resource/SupportTicketApiResource.java | 47 +- .../resource/SupportUserApiResource.java | 47 +- .../resource/TamUserApiResource.java | 28 +- .../resource/TicketImportApiResource.java | 24 +- .../resource/TicketRatingApiResource.java | 17 +- .../resource/TicketResource.java | 77 ++- .../resource/TicketWorkbenchApiResource.java | 31 +- .../resource/TimezoneResource.java | 17 +- .../resource/UserResource.java | 158 +++-- .../resource/UserTicketApiResource.java | 37 +- .../resource/UserViewApiResource.java | 38 +- .../service/UserProvisioningService.java | 75 +++ .../mnemosyne_systems/util/CurrentUser.java | 92 +++ .../main/resources/application.properties | 30 + .../resource/AccessTestSupport.java | 31 +- .../resource/AdminAccessTest.java | 366 ++++++----- .../resource/MessagePrivacyAccessTest.java | 246 +++++-- .../resource/PasswordResetResourceTest.java | 3 - .../resource/SuperuserAccessTest.java | 130 ++-- .../resource/SupportAccessTest.java | 617 ++++++++++-------- .../resource/TAMAccessTest.java | 169 ++--- .../resource/TicketImportResourceTest.java | 58 +- .../resource/TicketRatingAccessTest.java | 224 +++---- .../resource/UserAccessTest.java | 194 +++--- .../test/resources/application.properties | 16 + src/frontend/package-lock.json | 73 +-- src/frontend/package.json | 1 + src/frontend/src/auth/AuthContext.ts | 22 + src/frontend/src/auth/AuthProvider.tsx | 147 +++++ src/frontend/src/auth/keycloak.ts | 17 + src/frontend/src/auth/useAuth.ts | 14 + .../components/layout/AuthenticatedHeader.tsx | 12 +- .../layout/SessionInactivityManager.tsx | 13 +- src/frontend/src/main.tsx | 6 +- src/frontend/src/pages/LoginPage.tsx | 32 +- src/frontend/src/pages/PasswordPage.tsx | 137 +--- src/frontend/src/routes/CoreRoutes.tsx | 4 +- 84 files changed, 2924 insertions(+), 1991 deletions(-) create mode 100644 contrib/keycloak/.env-keycloak.example create mode 100644 contrib/keycloak/README.md create mode 100755 contrib/keycloak/create-user.sh create mode 100644 contrib/keycloak/generate_realm.py create mode 100644 contrib/keycloak/keycloak-realm.template.json create mode 100755 contrib/keycloak/login.sh create mode 100644 doc/manual/en/images/keycloak-login.png create mode 100644 doc/manual/en/images/keycloak-password-update.png create mode 100644 src/backend/main/java/ai/mnemosyne_systems/service/UserProvisioningService.java create mode 100644 src/backend/main/java/ai/mnemosyne_systems/util/CurrentUser.java create mode 100644 src/frontend/src/auth/AuthContext.ts create mode 100644 src/frontend/src/auth/AuthProvider.tsx create mode 100644 src/frontend/src/auth/keycloak.ts create mode 100644 src/frontend/src/auth/useAuth.ts diff --git a/.gitignore b/.gitignore index 3d480679..7686cb0b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,8 @@ src/frontend/dist/ src/backend/main/resources/META-INF/resources/app/ .m2/ tmp/ -.env \ No newline at end of file +.env + +# keycloak secrets +.env-keycloak +keycloak-realm.json \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index a35db942..d96441ff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,3 +4,4 @@ Jesper Pedersen Ahmed Mordi Hamza Azeem Shashank Singh +Omar Goher diff --git a/README.md b/README.md index 9e51e394..dd6ecc15 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ roles to navigate and get their work done quicker. ## Features - Support ticket system with 5 roles (User, Superuser, TAM, Support, Admin) +- Keycloak Single Sign-On (SSO) authentication - Branding - Public and private messages - Search across ticket numbers and messages @@ -36,6 +37,7 @@ roles to navigate and get their work done quicker. - [Developer guide](https://github.com/mnemosyne-systems/billetsys/blob/main/doc/DEVELOPERS.md) - [Build and run guide](https://github.com/mnemosyne-systems/billetsys/blob/main/doc/BUILDING.md) - [Frontend guide](https://github.com/mnemosyne-systems/billetsys/blob/main/src/frontend/README.md) +- [Keycloak integration guide](https://github.com/mnemosyne-systems/billetsys/blob/main/contrib/keycloak/README.md) See the [releases page](https://github.com/mnemosyne-systems/billetsys/releases) for downloads and release notes. @@ -45,6 +47,7 @@ for downloads and release notes. **billetsys** is built with - [Quarkus](https://quarkus.io/) on [Java 25](https://openjdk.org/) for the backend +- [Keycloak](https://www.keycloak.org/) for identity management and Single Sign-On (SSO) - [PostgreSQL](https://www.postgresql.org) for storage - [React](https://react.dev/) and [TypeScript](https://www.typescriptlang.org/) on the frontend, bundled with [Vite](https://vitejs.dev/) @@ -79,7 +82,15 @@ Copy the example environment file and fill in the secrets: cp .env.example .env ``` -Start the support services (CAP + Valkey) with compose: +Set up Keycloak environment and generate the realm file: +```sh +cd contrib/keycloak +cp .env-keycloak.example .env-keycloak +python generate_realm.py +cd ../.. +``` + +Start the support services (CAP + Valkey + keycloak) with compose: ```sh make platform diff --git a/contrib/keycloak/.env-keycloak.example b/contrib/keycloak/.env-keycloak.example new file mode 100644 index 00000000..d8c1358a --- /dev/null +++ b/contrib/keycloak/.env-keycloak.example @@ -0,0 +1,30 @@ +KEYCLOAK_REALM_NAME=billetsys +KEYCLOAK_SERVER_URL=http://localhost:8180 + +KEYCLOAK_ADMIN_USER=Admin +KEYCLOAK_ADMIN_EMAIL=admin@admin.com +KEYCLOAK_ADMIN_FIRSTNAME=admin +KEYCLOAK_ADMIN_LASTNAME=admin +KEYCLOAK_ADMIN_PASSWORD=admin + +KEYCLOAK_ENABLE_REGISTRATION=true + +# if enable reset password & verify email should configure SMTP keycloak mail +KEYCLOAK_ENABLE_VERIFY_EMAIL=true +KEYCLOAK_ENABLE_RESET_PASSWORD=true + +KEYCLOAK_BACKEND_CLIENT_ID=billetsys-backend +KEYCLOAK_BACKEND_CLIENT_SECRET=123456 +BACKEND_URL=http://localhost:8080 + +KEYCLOAK_FRONTEND_CLIENT_ID=billetsys-frontend +FRONTEND_URL=http://localhost:8080 + +KEYCLOAK_MAIL_HOST=smtp.example.com +KEYCLOAK_MAIL_PORT=587 +KEYCLOAK_MAIL_FROM=example@example.com +KEYCLOAK_MAIL_FROM_NAME=example +KEYCLOAK_MAIL_USERNAME=example@example.com +KEYCLOAK_MAIL_PASSWORD=change-me +KEYCLOAK_MAIL_START_TLS=true +KEYCLOAK_MAIL_SSL=false \ No newline at end of file diff --git a/contrib/keycloak/README.md b/contrib/keycloak/README.md new file mode 100644 index 00000000..bf2ac7ce --- /dev/null +++ b/contrib/keycloak/README.md @@ -0,0 +1,156 @@ +# Keycloak + +## Authentication Flow + +``` +[ User Browser ] + | + | 1. Accesses Frontend App + v +[ React App (keycloak-js) ] + | + | 2. Check Authentication (check-sso) + +---> [ Not Authenticated ] ---> Redirect to Keycloak Login Screen + | (Registration / Reset allowed if enabled) + | + | 3. User Logged In + v +[ Keycloak Issues JWT Token ] + | + | 4. Frontend receives Access Token + | 5. Global fetch interceptor injects header on /api/*: + | Authorization: Bearer + v +[ Quarkus Backend (quarkus-oidc) ] + | + | 6. Validates JWT Signature & Claims + | 7. JIT Provisioning & Role Mapping via UserProvisioningService + v +[ PostgreSQL DB (users table) ] +``` + +### Detailed Flow Steps: + +1. **Accessing the App**: The user accesses the React frontend application URL. +2. **SSO Check**: The React app initializes `keycloak-js` on load using `onLoad: "check-sso"`. +3. **Redirection to Keycloak**: If the user is not authenticated, visiting `/login` or protected areas triggers `keycloak.login()`, redirecting them to Keycloak. If user registration is enabled in Keycloak realm settings, users can also register a new account on this page. +4. **Token Issuance**: After successful authentication, Keycloak redirects the user back to the React app with a valid JWT access token. +5. **Automatic Token Attachment**: In `AuthProvider.tsx`, a global `window.fetch` interceptor automatically appends the Bearer token to all local API requests: + ```typescript + headers.set("Authorization", `Bearer ${keycloak.token}`); + ``` +6. **Background Token Refresh**: A 15-second interval loop continuously runs `keycloak.updateToken(30)` to refresh tokens before expiration without disturbing user experience. + +--- + +## User Synchronization & Just-In-Time (JIT) Provisioning + +### The Challenge +When a user registers or logs in via Keycloak, their account exists in Keycloak's database, but does not yet exist in the application's PostgreSQL `users` table. + +### The Solution: Just-In-Time (JIT) Provisioning + +#### **In Frontend** +On each session load, the frontend fetches application state and session data: +```typescript +GET /api/app/session +Headers: { + Authorization: "Bearer " + keycloak.token +} +``` + +#### **In Backend** +1. **Token Extraction**: Quarkus `quarkus-oidc` validates the incoming Bearer JWT token. +2. **Context Resolution (`CurrentUser.java`)**: The JAX-RS endpoint invokes `@Inject CurrentUser currentUser`. +3. **JIT Synchronization (`UserProvisioningService.java`)**: + * Reads JWT claims: `sub` (`keycloakId`), `email`, `preferred_username`, `name` (`fullName`), and `realm_access.roles`. + * Searches the database for an existing user matching `keycloakId` (or `email`). + * **If the user exists**: Updates/syncs their profile fields and maps Keycloak realm roles (`admin`, `support`, `tam`, `superuser`, `user`) to internal application types (`User.type`). + * **If the user does NOT exist**: Automatically provisions and persists a new `User` record in PostgreSQL. +4. **Request Caching**: The `@RequestScoped` `CurrentUser` bean caches the resolved user for the lifetime of the request, eliminating duplicate database lookups. + +--- + +## Configuration + +Authentication behavior can be customized via Keycloak realm settings in `contrib/keycloak/.env-keycloak`: + +### 1. Realm Feature Options +* **User Registration**: `KEYCLOAK_ENABLE_REGISTRATION=true` (Enables or disables self-registration on Keycloak's login page). +* **Email Verification**: `KEYCLOAK_ENABLE_VERIFY_EMAIL=true`. +* **Password Reset**: `KEYCLOAK_ENABLE_RESET_PASSWORD=true`. + +> [!NOTE] +> Enabling **Email Verification** or **Password Reset** requires configuring valid SMTP server credentials in `.env-keycloak`: +> ```env +> KEYCLOAK_MAIL_HOST=smtp.example.com +> KEYCLOAK_MAIL_PORT=587 +> KEYCLOAK_MAIL_FROM=no-reply@example.com +> KEYCLOAK_MAIL_USERNAME=example@example.com +> KEYCLOAK_MAIL_PASSWORD="your-password" +> KEYCLOAK_MAIL_START_TLS=true +> ``` + +### 2. Backend OIDC Properties (`src/backend/main/resources/application.properties`) +```properties +quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://localhost:8180}/realms/${KEYCLOAK_REALM_NAME:billetsys} +quarkus.oidc.client-id=${KEYCLOAK_BACKEND_CLIENT_ID:billetsys-backend} +quarkus.oidc.credentials.secret=${KEYCLOAK_BACKEND_CLIENT_SECRET:123456} +quarkus.oidc.application-type=service +quarkus.oidc.roles.role-claim-path=realm_access/roles +``` + +### 3. Frontend Client Configuration (`src/frontend/src/auth/keycloak.ts`) +```typescript +const keycloak = new Keycloak({ + url: import.meta.env.VITE_KEYCLOAK_URL || "http://localhost:8180", + realm: import.meta.env.VITE_KEYCLOAK_REALM || "billetsys", + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || "billetsys-frontend", +}); +``` + +--- + +## Running the keycloak + +### 1. Set up Environment & Generate Realm +```bash +cd contrib/keycloak +cp .env-keycloak.example .env-keycloak +python generate_realm.py +cd ../.. +``` + +### 2. Start Services via Docker Compose +```bash +docker compose up -d keycloak +``` + +Access keycloak at `http://localhost:8180`. + +--- + +## Helper Scripts & Testing + +```bash +# 1. Create a Keycloak user +./contrib/keycloak/create-user.sh + +# 2. Get a JWT token +./contrib/keycloak/login.sh + +# 3. Call REST API endpoint with Bearer token +TOKEN=$(./contrib/keycloak/login.sh user1 user1) +curl -s "http://localhost:8080/api/user/tickets" -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Role → Endpoint Mapping Reference + +| Role | Access Level | Example Protected Endpoint | +| :--- | :--- | :--- | +| **`admin`** | Administrator | `GET /api/admin/users`, `GET /api/companies` | +| **`support`** | Support Engineer | `GET /api/support/tickets`, `GET /api/support/users` | +| **`superuser`** | Superuser | `GET /api/superuser/tickets`, `GET /api/superuser/users` | +| **`tam`** | Technical Account Manager | `GET /api/user/tickets`, `GET /api/tam/users` | +| **`user`** | Standard User | `GET /api/user/tickets`, `GET /api/user/externals` | +| **`any / none`** | Public | `GET /api/app/session`, `GET /health` | diff --git a/contrib/keycloak/create-user.sh b/contrib/keycloak/create-user.sh new file mode 100755 index 00000000..2f13aa4c --- /dev/null +++ b/contrib/keycloak/create-user.sh @@ -0,0 +1,70 @@ +# Usage: +# ./create-user.sh +# +# Example: +# ./create-user.sh user1 user1@mnemosyne-systems.ai User@123 user John Doe + +USERNAME=$1 +EMAIL=$2 +PASSWORD=$3 +ROLE=$4 +FIRST_NAME=$5 +LAST_NAME=$6 + +# get keycloak admin token +KEYCLOAK_TOKEN=$(curl -s -X POST \ + "http://localhost:8180/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" \ + -d "username=admin" \ + -d "password=admin" | jq -r .access_token) + + +# create user +curl -s -X POST \ + "http://localhost:8180/admin/realms/billetsys/users" \ + -H "Authorization: Bearer $KEYCLOAK_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"$USERNAME\", + \"email\": \"$EMAIL\", + \"firstName\": \"$FIRST_NAME\", + \"lastName\": \"$LAST_NAME\", + \"enabled\": true, + \"emailVerified\": true, + \"requiredActions\": [], + \"credentials\": [{ + \"type\": \"password\", + \"value\": \"$PASSWORD\", + \"temporary\": false + }] + }" + + +# get user id +USER_ID=$(curl -s \ + "http://localhost:8180/admin/realms/billetsys/users?username=$USERNAME" \ + -H "Authorization: Bearer $KEYCLOAK_TOKEN" | jq -r '.[0].id') + +# get role id +ROLE_ID=$(curl -s \ + "http://localhost:8180/admin/realms/billetsys/roles/$ROLE" \ + -H "Authorization: Bearer $KEYCLOAK_TOKEN" | jq -r '.id') + +# assign role +curl -s -X POST \ + "http://localhost:8180/admin/realms/billetsys/users/$USER_ID/role-mappings/realm" \ + -H "Authorization: Bearer $KEYCLOAK_TOKEN" \ + -H "Content-Type: application/json" \ + -d "[{\"id\": \"$ROLE_ID\", \"name\": \"$ROLE\"}]" + + +echo "" +echo "Created user:" +echo " Username: $USERNAME" +echo " Email: $EMAIL" +echo " First Name: $FIRST_NAME" +echo " Last Name: $LAST_NAME" +echo " Role: $ROLE" +echo " User ID: $USER_ID" \ No newline at end of file diff --git a/contrib/keycloak/generate_realm.py b/contrib/keycloak/generate_realm.py new file mode 100644 index 00000000..cd5f20df --- /dev/null +++ b/contrib/keycloak/generate_realm.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import re + +BASE_DIR = Path(__file__).parent + +ENV_FILE = BASE_DIR / ".env-keycloak" +TEMPLATE_FILE = BASE_DIR / "keycloak-realm.template.json" +OUTPUT_FILE = BASE_DIR / "keycloak-realm.json" + + +def load_env_file(env_path): + values = {} + + for line in Path(env_path).read_text().splitlines(): + line = line.strip() + + # Skip comments and empty lines + if not line or line.startswith("#"): + continue + + key, value = line.split("=", 1) + + # Remove surrounding quotes + value = value.strip().strip('"').strip("'") + + values[key.strip()] = value + + return values + + +def replace_placeholders(template, values): + pattern = re.compile(r"\$\{([A-Z0-9_]+)\}") + + def replacer(match): + key = match.group(1) + return values.get(key, match.group(0)) + + return pattern.sub(replacer, template) + + +def main(): + env_values = load_env_file(ENV_FILE) + + template_content = Path(TEMPLATE_FILE).read_text() + + output_content = replace_placeholders( + template_content, + env_values + ) + + Path(OUTPUT_FILE).write_text(output_content) + + print(f"Generated {OUTPUT_FILE} successfully.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/contrib/keycloak/keycloak-realm.template.json b/contrib/keycloak/keycloak-realm.template.json new file mode 100644 index 00000000..d2093351 --- /dev/null +++ b/contrib/keycloak/keycloak-realm.template.json @@ -0,0 +1,128 @@ +{ + "realm": "${KEYCLOAK_REALM_NAME}", + "enabled": true, + "sslRequired": "external", + "defaultRoles": ["user"], + "verifyEmail": "${KEYCLOAK_ENABLE_VERIFY_EMAIL}", + "registrationAllowed": "${KEYCLOAK_ENABLE_REGISTRATION}", + "resetPasswordAllowed": "${KEYCLOAK_ENABLE_RESET_PASSWORD}", + "smtpServer": { + "host": "${KEYCLOAK_MAIL_HOST}", + "port": "${KEYCLOAK_MAIL_PORT}", + "from": "${KEYCLOAK_MAIL_FROM}", + "fromDisplayName": "${KEYCLOAK_MAIL_FROM_NAME}", + "auth": "true", + "user": "${KEYCLOAK_MAIL_USERNAME}", + "password": "${KEYCLOAK_MAIL_PASSWORD}", + "starttls": "${KEYCLOAK_MAIL_START_TLS}", + "ssl": "${KEYCLOAK_MAIL_SSL}" + }, + "roles": { + "realm": [ + { + "name": "user", + "description": "Regular user role" + }, + { + "name": "support", + "description": "Support role" + }, + { + "name": "superuser", + "description": "superuser role" + }, + { + "name": "tam", + "description": "tam role" + }, + { + "name": "admin", + "description": "Administrator role" + } + ] + }, + "clients": [ + { + "clientId": "${KEYCLOAK_BACKEND_CLIENT_ID}", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "bearerOnly": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "${KEYCLOAK_BACKEND_CLIENT_SECRET}", + "directAccessGrantsEnabled": true, + "standardFlowEnabled": true, + "redirectUris": [ + "${BACKEND_URL}/*" + ], + "webOrigins": [ + "${BACKEND_URL}" + ] + }, + { + "clientId": "${KEYCLOAK_FRONTEND_CLIENT_ID}", + "enabled": true, + "protocol": "openid-connect", + "publicClient": true, + "bearerOnly": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "redirectUris": [ + "${FRONTEND_URL}/*" + ], + "webOrigins": [ + "${FRONTEND_URL}" + ], + "attributes": { + "pkce.code.challenge.method": "S256" + } + } + ], + "users": [ + { + "username": "${KEYCLOAK_ADMIN_USER}", + "enabled": true, + "email": "${KEYCLOAK_ADMIN_EMAIL}", + "firstName": "${KEYCLOAK_ADMIN_FIRSTNAME}", + "lastName": "${KEYCLOAK_ADMIN_LASTNAME}", + "credentials": [ + { + "type": "password", + "value": "${KEYCLOAK_ADMIN_PASSWORD}" + } + ], + "realmRoles": ["admin"] + }, + { + "username": "support1", + "enabled": true, + "email": "support@support.com", + "firstName": "Sarah", + "lastName": "Johnson", + "credentials": [ + { + "type": "password", + "value": "support1" + } + ], + "realmRoles": ["support"] + }, + { + "username": "user1", + "enabled": true, + "email": "user@user.com", + "firstName": "John", + "lastName": "Doe", + "credentials": [ + { + "type": "password", + "value": "user1" + } + ], + "realmRoles": ["user"] + } + ] +} diff --git a/contrib/keycloak/login.sh b/contrib/keycloak/login.sh new file mode 100755 index 00000000..237fa2f7 --- /dev/null +++ b/contrib/keycloak/login.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Usage: +# ./login.sh +# +# Example: +# ./login.sh omar User@123 + +USERNAME=$1 +PASSWORD=$2 + +KEYCLOAK_URL="http://localhost:8180" +REALM="billetsys" +CLIENT_ID="billetsys-backend" + +TOKEN=$(curl -s -X POST \ + "$KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=123456" \ + -d "username=$USERNAME" \ + -d "password=$PASSWORD" | jq -r .access_token) + +echo "$TOKEN" \ No newline at end of file diff --git a/doc/BUILDING.md b/doc/BUILDING.md index 192ab9e0..df6f06ca 100644 --- a/doc/BUILDING.md +++ b/doc/BUILDING.md @@ -55,6 +55,7 @@ The root `docker-compose.yml` starts: - `cap` on `http://localhost:3000` - `valkey` for CAP storage +- `keycloak` on `http://localhost:8180` Then start billetsys itself in development mode: @@ -173,5 +174,5 @@ The targets are * `docs` - Generate documentation * `db-drop` - Drop the database * `db-create` - Create the database -* `platform` - Start compose services (CAP + Valkey) +* `platform` - Start compose services (CAP + Valkey + Keycloak) * `full` - Run everything (db-drop, db-create, platform, clean, format, test, docs, run) diff --git a/doc/DEVELOPERS.md b/doc/DEVELOPERS.md index 891f8c5f..66ebc779 100644 --- a/doc/DEVELOPERS.md +++ b/doc/DEVELOPERS.md @@ -185,14 +185,16 @@ psql -h localhost -p 5432 -U ticketdb ticketdb #### billetsys -``` sh -cd /usr/local -git clone https://github.com/mnemosyne-systems/billetsys.git -cd billetsys -``` - See `BUILDING.md` for the current development, build, test, and frontend integration commands. +## Keycloak OIDC Authentication + +Billetsys uses **Keycloak** for Single Sign-On (SSO) and bearer token authentication. + +For full setup details, environment variables, realm generation, helper scripts (`create-user.sh` & `login.sh`), CLI testing, and role-to-endpoint mappings, see the dedicated guide: + +**[Keycloak Integration & Testing Guide](../contrib/keycloak/README.md)** + ## Logging levels | Level | Description | diff --git a/doc/manual/en/22-security.md b/doc/manual/en/22-security.md index a9e09844..1a944667 100644 --- a/doc/manual/en/22-security.md +++ b/doc/manual/en/22-security.md @@ -10,44 +10,62 @@ The **Security** model in billetsys controls who can sign in, what they can acce Billetsys is built around differentiated roles and scoped access. Security is what makes that possible. It ensures that users reach the right parts of the system and that ticket, company, and administrative data are not treated as universally available. -## Authentication +## Keycloak -Authentication is the starting point of access control. A user signs in with account credentials, and the application uses that identity to establish an active session. +Billetsys uses **Keycloak** as its centralized identity provider. -This allows billetsys to recognize the user across requests and present the correct role-specific experience. +### Key Benefits: +* **Centralized Credentials**: Manage your credentials securely through Keycloak. +* **Automatic Session Renewal**: Your session stays active while you work without interrupting your tasks. +* **Role-Based Access Control**: Capabilities and navigation are automatically tailored based on your assigned Keycloak realm roles. +* **Self-Service Security**: Update passwords securely using Keycloak's self-service workflows. -## Authorization +--- -After sign-in, authorization determines what the user is allowed to do. This is where the role model becomes active. +## How to Sign In -In practice, authorization defines access across areas such as: +1. **Access Billetsys**: Open the application URL in your browser. +2. **Automatic SSO Redirection**: If you are not signed in, visiting protected areas will automatically redirect you to the official Keycloak SSO portal. +3. **Enter Credentials**: Enter your username and password on the Keycloak login screen and click **Sign In**. -* User-facing ticket views -* Company-scoped coordination views -* Support work areas -* Administrative configuration -* Reporting and oversight pages +![Keycloak Login Screen](images/keycloak-login.png){ width=100% } -This is one of the core design principles of billetsys because different roles are intentionally meant to see different scopes of work. +4. **Role Dashboard Routing**: After successful authentication, Keycloak redirects you back to your role-specific home area (e.g., Support Workbench, Admin Dashboard, or Customer Ticket Portal). -## Session behavior +--- -Security in billetsys also depends on session handling. Once a user is authenticated, the session keeps the application connected to that identity until the user signs out or the session ends. +## Account Provisioning & First-Time Sign-In -This makes the system usable in day-to-day work while still preserving the idea of authenticated access. +You do not need to manually pre-register inside Billetsys. On your first sign-in, Keycloak passes your authenticated profile (name, email, unique ID, and assigned roles) to Billetsys, which automatically provisions your account via **Just-In-Time (JIT) provisioning**. -## Logout and account boundaries +Subsequent logins dynamically sync any updated profile details and role assignments from Keycloak. -Signing out is an important part of the same model. It closes the active session and ensures that the next interaction requires authentication again. +--- -This is especially important in environments where multiple people may use the same device or shared workstation patterns exist. +## Active Sessions & Inactivity Timeout -## Password protection +* **Silent Token Renewal**: While actively working in Billetsys, background processes automatically renew your security tokens every 15 seconds. -Passwords are part of the security foundation of the system, but billetsys treats them as part of a broader access-control model rather than a standalone feature. +--- -Together with session handling and role enforcement, password protection helps keep the application aligned with the responsibilities assigned to each user type. +## Self-Service Password Updates -## Why it matters +To change your password: +1. Click your profile name/avatar in the top navigation bar. +2. Select **Change Password** (or navigate to `profile/password`). +3. You will be redirected to Keycloak's secure **Update Password** page. +4. Enter your new password, confirm it, and submit. -Without security controls, billetsys would lose the structure that makes its role model and customer scoping useful. With authentication, authorization, and session control in place, the platform can support collaboration while still preserving boundaries between users, roles, and organizations. +![Keycloak Password Update Screen](images/keycloak-password-update.png){ width=100% } + +Upon completion, Keycloak safely redirects you back to Billetsys with a success confirmation toast. + +--- + +## Signing Out (Logout) + +To end your session securely: +1. Click your profile avatar in the top navigation bar. +2. Click **Sign out**. + +Signing out invalidates your Keycloak session tokens across the application. diff --git a/doc/manual/en/25-password-reset.md b/doc/manual/en/25-password-reset.md index e3285171..ff0f011f 100644 --- a/doc/manual/en/25-password-reset.md +++ b/doc/manual/en/25-password-reset.md @@ -2,18 +2,21 @@ # Password Reset -The **Password Reset** flow provides a secure, self-service way for users to regain access to their accounts if they forget their credentials. +The **Password Reset** flow provides a secure, self-service way for users to regain access to their accounts if they forget their credentials. In Billetsys, user credentials and password policies are centrally managed by **Keycloak**. ## Purpose -To maintain security while minimizing support overhead, billetsys allows users to independently reset their passwords. This flow is protected by CAP (CAPTCHA) integration to prevent abuse, such as automated spam or enumeration attacks. +To maintain security while minimizing support overhead, billetsys integrates with Keycloak's self-service credential management workflows. Users can independently reset or update their passwords directly through Keycloak. -## The Reset Flow +## Keycloak Password Reset Flow + +1. **Initiating Reset**: Click "Forgot your password?" on the login page or navigate directly to the Keycloak authentication screen. +2. **Keycloak Verification**: Keycloak prompts for your registered email address or username. +3. **Email Delivery**: Keycloak sends a time-limited password reset link to your email address (if SMTP is configured in Keycloak). +4. **Updating Password**: Clicking the secure link takes you to Keycloak's **Update Password** page where you specify your new password. + +--- -1. **Requesting a Reset**: A user initiates the process from the login page by clicking "Forgot your password?". They are prompted to enter their email address. -2. **CAP Verification**: Before the request is accepted, the user must solve a CAP challenge. This ensures that the request is coming from a human and protects the system from automated bots. -3. **Email Delivery**: If the email matches an active account, billetsys sends an email containing the user's username and a secure, time-limited reset link. The email format follows that user's profile preference when one has been configured. -4. **Setting a New Password**: Clicking the link takes the user to a secure reset page where they can establish a new password and immediately regain access to the application. ## Security Considerations diff --git a/doc/manual/en/97-acknowledgement.md b/doc/manual/en/97-acknowledgement.md index 1e6b5300..d6bd9ec7 100644 --- a/doc/manual/en/97-acknowledgement.md +++ b/doc/manual/en/97-acknowledgement.md @@ -11,6 +11,7 @@ Jesper Pedersen Ahmed Mordi Hamza Azeem Shashank Singh +Omar Goher ``` ## Committers diff --git a/doc/manual/en/images/keycloak-login.png b/doc/manual/en/images/keycloak-login.png new file mode 100644 index 0000000000000000000000000000000000000000..e97c6b6d214f301770259a51ca6aa00523cf3692 GIT binary patch literal 234036 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Ŭ!{k2cSpzbv^X?pzZuIdtj>GQyKD2pbDW zv5=Op5DX`?Sw#Kg zB4q^+Bp8v1s=8jr%wOv=3v@Mpudj$yli`d*-F5_V4{L~Wa~<#yQA{y3R zb1?pcU}ez*$8k@PSF$P4=D5%?+=o%Vm@7Q-24Kz;U z@IHTac_rH1AOhmXJ^oqG5+2o$o+m`b^e9_$3w3{hjW`D_2z*a!KZP_}ssS~y$ePz7 z0QJ^(&6D(<(=8E4%pUQ(*Umh@yC%R*!oBaK{KEvDeeO55yxfjQKB*9|^>Y}DUPFk$ zyFOb*K0KK&RZyf;@Vq*Z8^+6F;50DyF^6EFaYQ9oj?NIq&78pzQtkz)#r@xBA2pQ` zGkOZ0BX*Q(h`~fbv)+sT`2oHOe}9nQax-P4KBssIhSbD|> z&IWVwbD|?N^yFR|wfWNt_4$r5EOFid3UX_|N!oYFxT!bTcIcya4RJoqTx7GmE4Lp{ z?rLsEb9Td$g*IV+g~+dgw8gY~8*fz}iGdwa9>5%vS03?jRp7|;A-G!H=(wlGW5)b~ zoiY0bAK*-PUNH8O@**cfP)XU|xXAW$;5fL@MHd%p7Cap;a<$%xJ+ zT1)6zrWHZDk~rLgQHO%>e?Xi9!a+9T8!l5BMxK76xTYgZ&!os;X$H>&|JC zH4O<(B_YVWiRD+zn9hCk@>93_%Xmnp{pFS4LbRjE69^SuwpW)aNLuZli&ec2?bC zDQFipJ!q?68lp14YkAD${5j}VSG`;~Nb00sRx;X7a8=d_>;KLF?);oIxrKA^NbJ5# zG{z2vABD<|1CBY?*zP=UxkZj{ZbJS6L7utQT0lEcAp2ERu&egOs{s7eGxsAkv|TB^ z={^pB54u{!TJTmSIgel=N_(V zVhaH;6}wcX%ICV-*8%s@2 z%MQuzfDf;P1yS*>vOl$JI^@P&WFOhlFchscn@U|;1A~CklXd6&yR4u9O1{Px3}`f=@`21 zeCE!ok41Sd#3|yZ_KhU6J8>-;+PXMxH$hgI6xY=)@~?RdM}s7h4m?1QF}HfGz9@3u zhvwb(eC&UKp2m4brV|y|A=e!Ip1k?IKX+WpPv`cy3F*!2frtM{%B0eT?T)RN5g z9t+HF>j>eho9fRVOM?CWJ0NvcP$Vw>69gGvNN*UXTRG@nK9XpSxv{;n@V&YDB+3e8 zzKN+$=!virR3!9jkXdPwOxkhP0JXvD$iqf{PSyB|Rvde^$YYaskO>$7q8!l=8B zbFel!`0`6gdaqB?(q#Ki5e{dl`}BTLy2wt+`p5FhQVDOuv^T+@J4p9A@T&$DOJY(n zB2D%{+runHGSW?Fbt>|%bjcH*=aF%hvt>5yL-&!E@ZvA z4*6T|$xzp5UH`OjPk`eK)s##Rh*y)ej6)}~IgavPQae^}Y-Z6C|4IJNJ*-SiDF$_# zJ_8QoU6Czr0`#Hh~$W|<9;%Z2iXVfRL8hAgxA$55fR@YtKvi5-`_-9ffdxP(Y2Qsb&WAW{ zuWGQ5ctt%fadv_zSI(%|#4GlTS!kP3=YrXaXy#U*dB}d}YYpc!`PtVi0f-)?~ZPk;7 zVdxf5LzN-dgdHCCStJ0Z))jvLfjOfnD8TJ$tf#1-zHjXTXrVQn9bbk!brd7^-s)-u z9C=u004z;3R)A22%@~ibSf6T3jI~<7RoRbt2kFlW69ykll{lz zuAE*)s>k!O($n_8M7kE-Reg?FrIkK<-@Pk9y>WMZ1vlT$4uWbB=%-UH?U=uE&ERcR zIDBIXEz`nfIy9%J2BUf@pOacX}w8Sb3CzGTx+2aiMnEJzOU0|F# z-_QV~1-!-83Z?cBx6izc8mn^?ko`yfmX4XLzMKjMu z^j_S>spP16CiY~G4E4(n1e~f#hnBw=^qcG((*0D=-~j|;cM%Lfz`c=6Q6ie=i)BvY z%X>U&!*j1PHkI2F``pJ082Vwa>eBtfSuXngcr!eVAHJ8DyVum@15Bcv(FTuQQiA?(SMp!vjdcO6Sy76;cB#&DD(LAgbQ0(M0gL z7n9W)@l(|XzAcquvZtcp)0Y^w!@}%I)5vE|m;0-`pWM%UJpNz8eW>UlzsKpSV<14E zL}!wznOJxJ>!b68ms!b-WB{NZwxb6V|E25$R((>yTXD-{=DToT_ud;5V^r_|WY(Bq z0k4}2Sv0}-l{WQ!u_QN+r$3kdfM50O-bYpX3_xp~S#AG6vC}wy?weLos!vc%<;d*s#s**(&l$P(c z)DI?J_#mq!F#~_^14hBSPM1izNFNFuCP~n#qgB$)=m){$lWul=4N{95TK9fijy~xnOSa{AcIjnQvwf}e) zv&mM_t)GI+0d8D!3kjnc^EBVD@TN6!slZo+&uNk@lG#AKqMMFg1uc)ch@;A+-@K$F zhQZI~5)+OkS5fquy1=b?NtC<rS4$a^Hu8TEvWW^)_S9f^>qFHbwE^s{imlayFma2g+5l_aW(e(xU0Tq`5 zc1f#>k5rciXV81FqsQqx-=;Kd01qhC@{GKf`2Sq7=XR&|8Yfj48dnsuKNH%gbb9&y zta8hK%{P1P?Z4QZ0DE5{&H1HbOErK0$8*u1Btoh0wx2#or0cMsB>LJHPI7>ryBlX> zP#3-|B5WpRRM{0)#G0sCRBkH1`{UMziA_9wl~7ca+;5vUrVih_V|~o4@@L zo6)cYH}Rasaw!qW00tU%|>r61sH_b^-iD0$a@{y<9FnKpM4*pU}z%~1XyEU|6h@ncFv z=GmLDIIM;n3&57^;2CQ3vTW#3Po4%vwT0pAX)6gcE{SmBuN7Xck~v| zrzju?L*om72QcE&hZf~96{H4)N$Z!=l~*qlZ~T3`l8gwYK2CD#jlVy*3xOU@e-)v9 zIXOzaSMo*Ut5Q;tVa}nx?(xDyq*x@P##>Xn;n$!D)86YT0Td;`PpO3ERc*cdYgc>Ny!KU@9u*p4=om+*`4q*5rE?n~ z(;t~7{aQbWu2OJavYI_JXb?o=wlTSxTyLj1guDTMeFV?e8F-@>)s*fAPA1vk0E$|i zbPX2VPiA95?=j-~F2!z&qkqd}4<@`jUL_*Six)V(?yKhY@R`c4pEh!0WFQ5v%NCj? za=AkG;?tY<1PiOOU@6=}OWJZn9(tBS8~8NXXc{=TlkR4}t)e+5IKn(QTq*9|_81*l zTf08eCgW~v65*peu{ylC`G0`BG73~gYkx8lI=OX9%dr;KXZ18zLXV%L)Mg> zwnvn>%`S(_>1@)5hU{q@2sHwgwUZkD`^_SrI_XX9be1tQ${;ZHetk-p997I1oh41g z|0}-6xlv?@>C`s+|I4(_7Jq#qCAbB*sbj5vuYoJ~&Pd!Ov+-Un4EQlU&v&yd2gr?)@2`fS$v$IDK-Kdd0{6kaBo zyL#Ff`}s~j6Q2Il}@ z#h-u7uo$!THC`byKe_87a0X#@nv$DIm-v@GS@A-wtGLknNT9Ln^Udhf*Vf*tUq+JQ z;nrTD+qer?Mvk>p-HX~A*;jrY3GHH!`-CM#-WW)5;_Y8V77a2oeqJu3$}1MGQqG<2 z+cPJQxNF4cKIY#C$3?{uglI(9j>Vwje)AT@cjw1Y@T;`k*eqhNc}0NPYAi5_nN=X% zL|m1QR6kC|)8OBOtB)MLjr>hNH2<@2{s>aEVGaQyGeig88|2Qg1f}1q|G~K?0W-Hyb~SPXRFOD&UFD7HGnnDbvF$! z@VK|tqjVOkgSboX4AW$2v$uVtgcx>Aj)e`|`fM;eCj8JJpJSA>0eXgNnf3RZiF*d) zgVk^s+F)j=&^rTbg8%hoUi;`?Z_604uU;~gh)_!-)SqB4YFmCP@mYQr`&)4777S%e ze@6W#XtZ1w7enMaUL=%`6-oE@0M+lS6@^&q=C>0_CHxK0Q&=}bjdmn4BK3(HqEtuq@lw8R&>E_H?eByWq= zf_m~f30wby>;F?)e|)nO$}^^8>#(i>`gQlTdT6|U z$ieoNVd^@?>T;204{;z#t3iKRs`=I3Gg2@lT56Z3zXhCD8)uT;H6>SUo=D=F18|At zuhjbcDts5<;4HB z_SI*}oOG>zdu)}5VLUaij8yDzKbYOvr6N#g8IMZ=So=PUt^3%KR~Nhfb5!^+g#~j6 zr04xVVU@Fijr-u&A7p+=**{RoPzXa5+q0RdNqtFX)(ud`gI4Lzt>;zXV-f{W;mU&+ zJAM-7@evDx+D|FY8o1$S3hbhEt35~n%lt;$M$cor5{x^H#Bg|O67Aq+PS>_94Vf*h9-*BpPQIsCiM@{J}{#h`G!7J#5WB*DxWD^D}a5R zhO{SKNyzz>b$3g8c%^)3WVfqUgU+-W-Ez!ju3VtDlLudA;kUc3wNuabf!OV7FJb21 z#NcMtk0MsoReR)z?MUR+!R(L{!}!<1%B77dsil)wQC=ni{N( zryG$LS$CSDFxwWG)i&GvVJ`rTKb%7kfRaOIV)HBlxK2AiV3z74H|JHF*f(5>NTEao zV4bhZuV>a_oR^l@JkrNx;}0O@`BN0J7CG^-sz>W3|H>jb*(2*MZO#xzKNh_J^V&{! zLmwJ9&Tm}_@fL0Ue=o=G#w{bV%sCzsHlC}z?z4)imNXO=LQEQtdwR3Fc_o<_$6BMY zjGIAq`Q{$RC}gFhNm>bgs@3P0y|38l6E=jN6CKh13LgMp=Z{S2@BV*YfIj@pq_i9{ zBoE6~C|J|b5YZ%go}p%YmC^%)=@7%~Pr1IBl|gPhrd2$kIHnwh3yVJ#LmL|UdYVz$ zMFL(^;epRuv`YJBM1+fW>}3D@u@QqbVUMo~8|VWSq}D(3D4q4s)%W5U4Hk!Dh4)t| zznq#$W{!%BcwVI$rJ$@acGu#^fDY~qhR#|Jdv$Uxe_H;zrpL(bih*w@C^EidyYU|A zCVL{c&4NV0f$GZF8*zm;o9n&0_S4eW%_N#d|1{|!a*m!G*)R8ttDexIHAKnDAK>jy zX03jy@zJt3kxIDrX-JWBvp6orW|hVGBb}9G*%1vY*-L%{c%S1bxDldS)+haz|2O}J zuRch3mz(iuE9J3~pu9fVIGz>srJeDV{6>(hbfm+hlatB8cw@!gMQ>(EDUP7mp5$NM z$B4f+Jpj=uR+Rbco+s=8FxL%RDW6M7n)!~Zf~nwKl!%)Gf%B|1&N(r02k0dk?e1yU zFRpXvKz6@Rw{P1LT>d%gR{bpIZk~=aXKw$ndOnS()nWMdHu$9AjB{F2pp2$&w_QUe`+qKToo1Pdx}ps6T2j{SwIIC zQ{|ged`I@@AJV)&Tcl+_^n9&wFj?m-zq{`;xk)q2czVb)rQ`d9L(Z%f(eGpyNy zAN3?g6UD=s2>&Qo?2zry;TxP~;uiDP_Vu#?F`V*h&~CP*f9kHlqnJNh`ODjmvvw!x z|Is~y@9?mS&L$vx>MY!kw~Wcw14(m+VOdzb+`wO+=71o#=t}x?Nn5s!lK|P zoM?4x<8ykT|GY3Ad4Bp|!j4#Vs$!83{!vg&(^|^l&FLikMd;yeoD{8g<1J4mG)vp+ z-K!@^xby!*({+cl`F`*AqpdDgRLxSGsy$)^RjYPYTbtUeW)Lx}TAS3~v?yAuwh*iK zh}f}0&DbNc3BT9(x_ATPVOD7clgqepfedwv^?AMkYOkgzGmyiY@)hJCaUbz$=6Y*4NgcvzfPJ{ z*{2*%gQ(Pv?&N7nms43&_ARfypE&1)b|Q80a?;Y7tVSekd+MLm=Nd9L%Y?qHuawXK z+hx)RAY(jeCz>wvR>XbTqTTaZ=5~nrMy!f=W*4S?Ad!tFt|Ka+=5#I8m}@U$!BGN$ zjO;M#{+-Zeu1q>`_)Jt8lYWDs5QR`Vgk(yIAE}dF;%@cg5p+=?S!m=vERFtT`;f+R zOkXyAgFi$E=F~HB+j45SK|D=j*UgvK=`(fC(naKTN(57jkW1NH!I&9A%uC44=+;0J zkXXoSvLx5b2yaZi{=xkVYmv}irK3y=)QpJDQ?*+!o+rsIk6OiKbjgp0b`KdY zD!KEG?(+`+$(uVEc9ruiW036q_7bQJ6P`!C_dyh9g1WBVku7p9VO}=XjgGn;FL;uBtMa_!7IIy`u(ZlPjf<>S008}u{IZwq~NFUM)3%fzZ~usG{rkMLiw_=D2ShChN1k-Nj$ z>k_g8n~s(NKW4t(5eTuY*xy(>|2s6&tBLFFDd?s_&MW2n2EO^0s#ExqiR{{hwnC$N4u|8Llj$kJK!bOnZopGy3E^3wDW)kl|#z zJXiCf|K_PvXXl{ika{KE?~)ec(V{ayZ!`1nh;ARODRRcd7CVinKP= z!yVbeo!VrC-b7{?NB!Q#D`%}vzlnFzQqnNKADv;q7K@*-)ue`fr%xSfS2^eKZ-$~? zFQ1nA2^cQluUoFM=f_mHHil1igdP-s{Lm63I`BVQ`A3h=@)wZk;8W4tpT=QdI^Rs}I~3MPG^ z#vKZj2#Ub_T}l2r^_iq@cr~cREq1|e9~!**-r~P+0~;Q1>7S?1Sc|>~&@7q6jy^az zx}#1naP0~>`iRxZ^2=A>4UH%N8K@}ePTfy-(1tGhiY!bln)n{*RR?~Ia%)Lo?6gu! z4`1fSbot!do5x_wdF6&Vol^39Xe*5`Uu;UOvWGCmG=mA4U8P zW?AY7wMs+)7ekh0hX}0R*54>1va%`0T|^nS>LrwP7(-~Rwld{@)-X=*q{ahG_G&M? zjv~hQs;akJ#KXyi5$JQna44~imrfo;Fi?Vc@JI%Yu|nQ4E7@iPs0>#S-#{~V47W@Z_Z`%FdY z>5kVZ?baVNsLzK+wVbYqd}0D5%#s4PXytbFdr^^|G#iu#XTTGO9S)_N^vn{YBah}nbEP~|Kz4DUO4 zHVbVcWP+OZqIdjve3hD=i+Th1Xad~|q_Jx#mjJ>&)7}62w^U@-b>O};i%k~?WpYa_ z`3e&3C1rpp01E-j(D6+&s|WLc`N0aji|`tW4Yu`dQN-K zW{;XSjB!$_^@#)0JIGq(GGFP}=8KKyfTM;vV!sF)xfku?2Pl|3z&Fe;Pcd?YRJ5`8 z{MW0`g~*upzmv*r*=zIu1Rwm;_6J^ef!tpRXh0H|A$Q&;cRmCU*bh?yFT|W0toa!i?wNwRzWNLjcTk-|x+>h|gt@}0iCC5Z$ zg<6bB%i;32L2?>mc2vwKZEwLi`(|*RDjl18sW?P?@sqH{Pp9s4rdbpfD)O`@H^^Px zL0zyy|JJl8v3G*bjnHHS8YBULvGI`@sH59n0J%NCTXCl=X0}kmCN4G?o|Ih?^u-Yz zmzr{^+1&ZjZYnpOf8C&a)1T&_Z@JFp5S=sHE}D+KC#c`L^1d()HtxL8NJzsm%<8WQ z6QbJKkJZ4pq*drd@jfj?lSXzGygbn^6Rt556-9)h-c00W+&;1@yO0+)CMF6#_f&-Hg@K4V?7heBIJ>2v2RW@puA4a6;%%h3YagEr=iUGl)=wnFAi zpY>mGSihwjPnrLgqX1%wlB5rP-j6FJ!N`Neq=Bou-21FxFNhN|lP(mvIFGTGRV z7UfbZl@zsBenS^LN6+b}qd$)Pv)u&=B?e^u%0lC~w>fLm{aZPDWb|O|4T8Axrzt76 z{zDRvPll!6)6Qucfs-Y0CzfBp+BI2gX2?0s^o~Lvfz*Ev-8gYmW8ncuD?w8ox!=q^ z7JD$X@2vFf-UBDJHgQxNdpcfwft@2_8-M_Egv7}(pG|Z%0c+>35Ijinku0m!tkgYt zOjPsW1=&Z#y3Ll;=(*$Q9Uw+nSH5st{CRc^@NWUjdI2}jFwN(E&Bnmd4jh1FvFRgf z-UZ|`U{}j7Ps`8}i&2*sJD2|p3|D)jGMl&ww90bv8A;D_>!brUpP+Vt7sezPa2^9i zF3z2!=8RoiZyJsfvMXoi+KnmLV~X1iF0U#!++8n-)W%+7!7HTL>>0DJd* z%#4^=b+M)$cq)>8qTxj31R;8^!ksU6!xUOSk_i+yTyz4@`FxMRCF-x)Lq&}GpQe|R z^uD+c#K%2TZyCAV2FB=etg*=_zHbDpll(>`BqRzarGKJjA&_FIEIpIR*A2`w#J}CZ zcyNRsjoG$uLg)c0k4g;VufJe4u&L^m`rFE3&vFW1xW|a785fqbL@SJlQ)v?I^Wv9a zjjx=Y_HBggL--Z9m#DP)nE;NQJ+y;8q0c6)Gd?bA=7!T9b6!pj%^3Rnl}DC$b`tT9 z<3Rx?h)QUi@ep=`FSue?ZIv%c1Fs_olKujh*VH@>AZAsvT!l32E?BweoBq~tE@%r2Ss1d*%rH}Ymc+*IOpdkwdj zGjjg`25B-JzU36&Bed9e6`D;vhLmW44}`2E8B?gQA7l z8G|}P=8oUEfh|zhodKPN0a^dI`s#k*ieW1WTx6L0Hr{+t;^My>%>4W(a`&qM_-Fx@@hCA4>=wd^o897)dfDY2wNZoLbiYsqBtERV&V9~AT}l37Xkv1x6yN@GgG$0@4TSNM@Ch7R?k z_HzQ`FJY%_cvy;~&t9aN-wv<~0GkQ4uI9ACOv(y;UQ~>3!r64aOZexNpK6U`a71K5IXxY$z^{0xyrg3i!>d$ej*F^4H%! z>nJiKu3sS#;FN1-o%Gwv0&pfxoJ5DutHjZyC=0ch1OO5se1K)=q&JEy9X5bV2;^zp zYa@ErN*>?}Oh6CZ&t)bKhRQ0o^!KX_6z6+mX$1DM}V#`&7#q(%zXPaK^3k97oClQQ-Nk)OIhl+sDK=KLX=A0 z`!8_Xd?G5!hqEs_!=vQmW#RPGq@bHl;wj%Vr)o+cDsH7kqNwy)Nmwnt%kzHGvc-Qx zuHAM@R>v%W4E9yH|+bb zk5dCx=SXI=6A~bEcaq`jxW#CiTSorcsIE5OPrr5D>8x1Fx5o?$LkqV*DBe=>wLK2%Dvfo#+2=DgfHW44N*9)ayM2bCb`y zMJY-CW$eS9#7X8jc38aVtke;d>Rxt!MYJ)Z#9fPfrG%%HU2ehv%?AVMf4qI`V0~Kl zo|iX5ByJD$Pky&#)d5a z((aapD=MvD_rWyGMmu}3)w+hr0(uAX3gKeR)pY32go?Md%k{J{=5A zdSkl#nZ_8loX9y&tvph{+dq)}m3!GYloxtXoFBlGUlf)RUfnt#wPYO3IOuxYTX}*X z!*;vwm;s;zY?#!_HF`WDbs^el0jUa$H(Q&E=pZRV-EW}9X9y3yFB^*U#SO38IYh;; z$eO)#Q7IIY!mzVVyWPSED_|fa)@>88NA?IMttKDz=Z%Z&UQRM@pL0*9h{i3wxYZ&= z$uU$})Lmm`s^lI39<9DH&+Y=W&3cpM3SmQeM}Bg5qJYavz1|^}&(kpP0qd+jLw`G- zjj|j0-E9_aBwNZDm<6+7ig`~Px$IvIV$Olb$#i{KIGvzo`>Ux(>76^{iiWKJ{gNEM z3xBP)FxftscrT-Yc#_=Zwhe|V$dR^vucK$W*bQZtbYEJC6u4O8lItOMfZ0<% zjOZeZ%pyEHu$x$Jhmx6}D1CmqnDRj_|HI1c#y*p6d?Ann0!PlTQK*$Lk|%l+d!)a=_Ec1)BMuU&Em-j&>qUF6G z)bR-AZ>ysm4FSFajX*~sZ2>`#wD1-F{q}oXhU{zz=~xqidP5-!cC6-^tV^fpug|HU zmeUy__D_$}_{Jw&Z^9Cr{(Rd|2o(O#hYsQpb%EEW?F`re;-0{W<46|u0Cq4>B}lad zf$C2HD&WSxi`mxwD$?|{HoezV{NN{e9Rm&q3@`8SGi0Nd9+Vm$L7pl>zuy~GL^8g} zYHcUJU3}Pc8ln2>Heo6|R66%gk|TMKEe)$X#l1QkQESL{NcYS|=_V_MwuJ@j@`8dN zBOBKFj)fZVXJ2t{wYw&F9OEn#n0X%-mKKXMFYgI)sXgSk)#ijO0zD9L8IS=szy!)$ zhQyx}&`|X6!Lf1|`Ws6T4eGEdF!cB3c-u)-z-Oj<f(}rAP4?E)j46Tceu2GD zkGiS*RVwdQ+kl?`@PRG#!hn3{sEhNs6 z^l4+yv3I!az^0bZ=xx)};bgUqD+tby-2JQ0k5DvTguJ(zo%bXx>L>Un3Ku=WMEU&| z^(D*Ce$1W?csfToOqeMy3$@K<9W~Yy0g=onkXhEHi6Zv)%ASfKL5TH};>~`QZ6?)r z-GFMEHBn5D06_b)oIEHttH~L2C=%8u>vz7zA(92vf#kP^f3Nx@_DQr4M{va$|5tyn zsr^sdv>%gMrpId6_y;g)=g02pMe*Nm>7zcK9~k`6o`=pUua;yoAhlk2oHMN{8I_FU z$}LeDcSpqWtA={LB}+Q%z+^|Dvk#^QjPz3!PkBxrT9pDyG~N_@>XPUDN-x|B#G9jy zLw#6ZA?e-L|M+bwKuI$Ef%0uy(c7MrWx=1f>Um1aRBqX_GMH1s)@_t%9^A;L%q^*` zl~iarso@8?G`#9<916Gia;$Pv9F7z3R4OY$#14%h)X+ZTm01VHwur;4%gFnr&+KAZ zAW(9hrgU$+nP5|(I%3oQEM8B!at7{=lb&14A^szkt80q+qEPTIb>sN61`lBjz^nkU z{i~aOI%iQ#2Hxs96ns&e1G>nKg9yRVX&Jnkv zz+Tw8N`0uZkK4I`Nc=M)xpC7v*~E*kb(h!dB%;dH(ZsETI@#36-ieqK$$z>))10F1 zGC=6~f-iCyePKh2Sn%3iPU>r}re4!MXj0Y-;FJ`1`nVwjbT7cI)L(C%Sm~v>*j^QC zY3RHj>sB?&(F0g~%AgucFbRDAElJXX*_D5EVH9+?{(Ky{afI+FR{tE<%pnmSS-Ade zuIhJ>kY07hjmjkWA$^_SreAap;1JEV8JCkv^Zd;!Q$7h-LmN`$4pLLl%p#q1IG@O)`Ag>3r($FZ&ZHOZc!ZYIp+JXmqE#o4vak3$_g;vzYcRviem;oUtJg$X{dw@m1uRR`Sus3TCu?bC=>hxXv$%dXMX8tNMDslEKx?1fcAh>4l=W&??oGiNp0({$U2Igoj<(nPb{!Tut}+?fJ} zzNZ#(x_CyWHuZNoNdBGS8*RPNk7Pab4zrI@%R z7;fS+v#fInoXX~_mCR}VFg?2I6T#n|<@My~O>dVR0BHR;E#FPA6=YxB3w!p2>Hoa| z&Aaz%8@HTXe5a>p_hO)aqq$56pGd*~_TYi%9LdHnbIz{_jl?p)WnJKg9?_Qb2Sx_~ zn*@!0U4MRc^sk<6pQe=|;dTl<9JZ3$d>q;A-7t;aKHggd_|#lx09MPc2*FPPyti^{ zYJ{oalnwOU3h)%$z4!_VVtAxGoCq_L+Mb92Q^;hvJrBnjO7vAsewW{j+GW;fuE!f9 zV>-Q?lm+Idrz0uRD5EUssq}18K!8KJk(;d z6f~obr_A=$zodpaHxeBAN};V*_&=%Hs5&@>%S|$-N)gNEBE%#=-ad` z-ju~iWTPA6|3;oV#7;= zLxBx%SgG~p8c#ToGf{AoNGMX5mvF}nLH;Z$4d_KFRnpK@kFyZI0yP@XZ^W~g=nqF> zs)#en80vc_?XSXkuY&pU->aYuq^L3gljUf3HdQ$wwE=L~FyJoW|5~n+yfwxi#F|ZB z?<1>lgN_fKe)M-mq_y8*ilS77q1dTmv!d^UAa_~Wbn~zV*aTM$imtZI!Bw8abkePY z-`gc~@ot+02o7KzK_ftGoC85;xP4IRprVie>bKX8KDa;|re!Wq&~gW(x@drshYU!P zYL0;+OsSD1a}5y{;dMr^!6H_-ar-8Ub`C>~Ch4pHAvwOrZEut&FrQ z+4!8jF9a#SS7n-UXQjf-k{V@whI$}{qo@L}m7M}i*MYd&m+2Np6^p4EutlL{L5en3 zL(oS8UpVg6_b>Rj58+SG!C7|{hlC0_Qd70x%Npf)Q3taqehE_==F6F>X5yV@nf~gO zDrO#V?twl{&0ph^OU(jVEk&&Lw5BYG%7xx)WNa)=9VnMIh~IE5$Dnp|2i zh^<}-&TixjN18y+GesC@Eka0>=Ehwnp3L=$@#SQ*i*>!``ULmGNn6VX(siA5x}Vrb zV!}b{&p|R21o>nT`ihOa>Uf}%CQUnp$rjVDRiOcGx((1b42zT!X~QelDKmZ zD%AfiWj__|w*rKeqLR5KPEdHg>S+4DXBBu|pfSIGHq|)dG?t&ShcgXWEb#R_J|TLI zgID58s#wRU-%&bg!6k$(PS@pdZ!kW*ZjG6P5i&Cva{0oSW@Rm0Ur4%tv5X^^!oa~0 zt*)xZ?Yv$;JISJQ*}*Ggasbe zG5NDJU}(R)L#~%GcuPM^)JgeTHB&JBP$?T944lBccbc?c(=*FaDGzG3)U;elNsI2o zbsA?l-+%V;{W+azJb7GNmi|`
-dFu2nYZFHwAbF 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%0D=t4Lwtb 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 z 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

D> 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≀6btexNw>YbCZcT}rH|U0dZdaP&LE=YgD^eSG zd=iJ}WW-HJX_62`BJ0Ki!^H%+@faIM$GCD?e1El76iKp-HYBu72iz3^#n-qh?pxDk zw!-euJ1Xwei38|2k;^u%7sx*PGw}TusqAThvUrOq|7*hK$VcaPf@QQ@QpZv9;z z`s6*ePd?As*3oY5DqeJ3&GQ)27)?<=e_p9&FP6_it%iZw1Y`?=_GTPLlE`J&q-T5{ zgN#<*P{<8ga^W(LKFBbmYSI2#)ElcA z59r(kq7os8};}g;6=}v=~hiBwGEb(hK z^SDDM=(sG0>n7gnyUEIUz4xx|1`u=#sA##I&jZWQM6P^YJ})%8tu?#-0=fP|(kakK zcUJD*vL8YWS{0r#_SpVnvwYQhb%(Yd-)RGz0JP6QdntUrmh| zyqIwR5q9Lk=m}vZUJ^S)T}hyS#^dh5LGFV=K^+6&KsYILm`#@3r1sUk)bX&WvFpg= zffZP`+oUB>ymPnyuLsJu?9aw0(T`Mo7&BvT6)V|5crHK$MpGHM*(ylrmSgc71D8W9 zp3d%#y9JJkO~^M^)VyGk(zYYI__Se6E&ru72Y)x17>u-QEPsW1f(3@t9Lh|T7UG{R zaMii^I-boNt~m+tQ!G!Z%~dPMhNWz`SI)~J{mL3{SD=;O{AN-X2xDezaB z3veMs5w-LoQ?dY>#MS|uQm06DdRg&E3O~(s5_d}KyO9ZL~x5aWRS{h z%g!xfP%9QKi*;yc9k^wx8<+4P^G z@6q2RxB<^N;9|-PJE54tnYx+^Vx*eRs_%Ec9QzOj6@+ zbsJb>{v_~FmB18MN2ttTd3r|oj>>V$_zcUB%inTlov~Jxsy6a1+PFSo)Gv-QpHQhv zrto-UGxuR@~IPLaW{xS3Kl8BXBOz>q{sesn>PSFXxs6wH!o+35+f8-|m zIMN+?S5>=A<#BYp*ead0v9LTWGYipPzyy63dW}|Y^mGK!C-zDPCJtSz&+-@%KlX+C z=bYyQqz}xAC6VwhDEd%ewcX$SO@GjUQXXsO1U4<1>rE(VTM&XoJCExDAPRMSkJ>l8 zziC4bwk^)jqnVq%7M%JR{JB+w(1}Y_r>LkXMM##TPU&TDrfGvtRQp;`23M?h$+|9gKpO_{ zV;*oa7FM+!?YoDQ#4#=UyQ#tIy{Rf9enIi;PE2+sjRMG3k#+yOyCx3E83F*|dlUB~ z3qD)!i*+_zzi#}f=*nhNh4t3(nK7W%{%HsAXjN+F*@-9URHfD=UZ|;FZj(%m@uEWL zJlEo*`PKT)dTsE14U9E{NKs$ho3)g;)B<4^`@G;mR@1PD^u*kVVy*&Nt_wETz8<>s zuy7(lEtfz*GE8uwZ>eo0S}TV^!Q$YahaN}1EO5mHB$N~|28@I} z`Pj?zqZtihaU*REH|8$$o99`ilog`?^@o7qBSu1vC0P3Mbdc}?zILyPXY-c2R&h?xv@w zEzy6aYD4reqTrE?$W@%6^E^iBlWsDo*hXA^+udQHeBh>X;N~ZMUU*T^^#sswBC<7b zMvH)1>iYby?V@U-TjEN-i2iy+_P$VgRaN=W|0wUs;{+_d>W!*Txvv1;u^OxRT~qxJ z3jpTmXp>&6tPeaFX+z($GYoHe^Eq-c|Alr5nRtNfGP|LhUjo*S4x2-hyI* z5En>hZ$RKST~NhQEb`Nw)m(>%h+eE$$&G`Lwz`-85_gSIik^{ zqt5|wCV|!)kz3Xfv6`HHFEzg~sPFe6OwpBiPDWSaol>JzTwn}veG5tLa_rj6!5#AX zNMuV$ypj>nrln9JA1rK_1&Bb{QBq{uR5dpKFu{48fupXNqL}TgEulF$ZL3LUL{rBS zQuF9rk^b>-d2+3fe`^CbPZqZkaTs!)54FCPZbtOZ^d#n;anRj`?1iqZ=dke1-`t*} z!l$Ri^y26F%$PD`z|C%vncr>!(0_hH4j1UtNhWg=4K(%`7XJIY3U4H1AOeW&1~wmg zR!F+8w*M_8PI{<E_4wJrYogfd~x2z8*;RS80&aAk@JeDuFpo`~+ zSdu3nj_+>M?^nD_yw?O#!^-zJ5XjX+&{_D`wzI2ypBY*i*TMLeZ;bfJUV2GzKo`~h zYNksM_0fEEkn3=L;Lc=IeWDaH@apD3bH$(=J{&anm>D^9#t6@pMa@^V9haEhO<&I3 z+ufp<;4g4m;lRLboJW2k=VBP~wd~5!SaagY{hM zDB)smYfYY?!K^kLyL0}0KZ%(rG9|g->5G}RVx3v3^u->~Ftcan#f*=0VPwpQB&8Sm ztF9LEk$8@)Q=}-zI&A%@4hMXZbqI{1dl( z!65T9WjgqV>^U75W(+ZcO=?LIF-|9`bMQ94VpR8I)Jck2e`w&JK4;?!e3`Iw^Bh4pTFHF!ThRCM7VRJd5x$*|0mwPX-5gK- z;%St!)OxC}RwEj*54j$)>1_L3%cIeq4Cr$iNpg9(a1|9V+WB8BN$9-S_M@cme7t;M$Ed*5Aom&SnGTQmQq zyP(7Q{Z=nnHG}U}BQvqYb|P`h=1L&a1+^9T?tThta`~B2>Y`saK{_=`;RxL1i!1qt3A=%w+E}EQuCG=`VAE_d_oCPPl8F#0&R0Z9aQjLD$4r z_q7g;cQ<@8JFStRmAgq+6`V)gSjRctybZ;D0J*H}TTH^K?i`^VsGXv8{E@!kmx*su zcCdO$f3L1~1Lm9I0otT*ci5toC+lXLYPhdxe@T2N#BQAZh)rzV_yXO&Sy));X3m-! z>R@1Bt_!E`ENRE!$CG%AE(`9)5)fiy$HZ z)vqEpz>z3{Z%IXCp*SlKrXmDn>{7E5?_LDxG2Vm12FDPgpsYE3ewRJD6pe=5Vb<-! z*eUPq!mT;p{h2V^X9;Um+t)O1C=VgE!SrwI%XXQ_YG%z$lOXY+D^cAj5l-&ldd`8S zROqylX>8|dYGmd7Y8{+u#W%gIal5=#BKBq@BiM?qfSS;Fq30Asn6tJLL>o2v@}6MP zRohH$2vGiDVZnjEs37fB;?pg&4J7rz@S9+bbom1rLP?P{K2Wqk1Q(N*<%|O_jv36) zLYDdi+aGemO^Mm9;@#5559pa$1w3+QIj|yNLLMk}u;9&uf`S)+3xxM&OSCRXz?TT6 zray7bxhe8PhN405TfWaew!h2BJWW;n@XA~EP)P&CbS8* zyh%EV50`%6=zp=g7Sb%>5rZul5$3W1cZDih306L>z}xrjU1+;dnS5M%Mjf+P4#d6 z-@kv^J>$F*IAaDngSs?Z-kX1w{kWGDgjk=rWE7EE`umxY_I|H>sb4X^Sq)NETUIX= z;E9v3q#^VB)YD|i6Z`Fa%;!AetTS41=?N5KNhMZ8=Gs~yA*LJ}_~))L-pYs5VNB;0 z)u4;->>p}!E+!PYZzYQW9F))(r%k6|^L56yV$5H1_~3J}~ZXh#u_e-1EbB7n4<$tmxjK zCn~>a&Su_YKb;d*rn#~Z9`8kTs0%)v6FV*dvjezf1Fik}FA~Pmd5z7!b%pk>BN=iwY1VnWqOf0bO<6JJDOk)+{5I(| zAQ_B6vPs_lsk2b}LBfSV#>95ItN8Iw=e5E7uwTB}i*a>HCDAvxd)BY6yKz#D+uX)< zlKAL!yxcOt&?kPqBdnu~Cj)TWv(F-1=7tSWXIANcE5$Q%(qiaHO;Ru?pj5umbU-pi z&ZHQ+e_TUnd-N6{`gf8BuXso%_d2IGVk%Iu!GGbKcfx!zlQd%NsK*je)urj)pQumk zZT@CRIVxTR98?F5ae^%To)ePf4X18zrj|mp-=+33Z_m7(*nGO6^(k>^_xJ3A-2TXh zikDj;Aqwzne+~=t9TRT0XI~x`Mn!eB*?uFfml4yn#%k>ratJq53zQ@XwA-Wxt*)D{WrqLuR$x?ij#~-JOdE~Z*>*B zSeW&7d7m5*eCVfsFIP>kEkhY~8*r-`(<%eO3N(8=8e@dqP|6iqkql0m;!G)6j70cz zbW>Plg%)58rxn@F&(E{?C%Jkp@_vO~GdRAHq*e5&6@y{R7}-ypURS~BNTi&p?wR9F3nLQ%wneIHvLGJ2Nu{KwHs~X;E+_S$G!WgHn0b5ieKU{57k1G57y(|K;C`<09!1@_C z*N>Z$++xe3&1*&2Iq8qf2yZF z$kP#MpuYOojn#c0?mw&(qxn>2#cdJ(Tt4pDfv4iQ1NcuC{NFZgI?`W#yI72O++Rhj z8u|n!jTzRS(J+O4Z$wb-3|&CF(n?Wbk;2WB`Wm(rjA0vAv$>;|@zYUsfebh>#g(+@ zuRp4Px-8Fgi@@~?)MEto=pQeZREz7pNqpTAw?qVY9qz5vHnn)65uM&Z$13Sa)QElW zU@P+vl|o3(`3kcCWlHQ;Ir+)e=bQ=Ys)x6w@s;!76mwc}{o=Th}yFt1vayWL+rUil#zi>$@YKSicZ)(alZijcvuZU6N39kXpKan72*sZMQ3K z5{gnKbK-FqLSlNjSI~TC3`YFdH3Q%Drr=*u3mHW=|s*Nc*tnJ z>^9&n%KtVHS3lzKW!hb?^D|r3{NST4LI3YuSpPC6&Ii3>0AAmC7E&7KoS58Ig(=lG ztEHGo^*iQ$%|Vz;e*S>#NW*r7K1e`mwvbc~y}{z>=;-1izi!v4f>ED%92`b6o+W#N zp0trl>)_tdNR42YM|F&HOJAAA9%XtOtba5Xg&HPE|dyD7qt|MAOp9`%X z&FU(lb*m`G8teX&ZB5noH_3Sa{+lFX88~j1V!D99_i2U-@uu^$2TXOPxopM?eD_8j znm6SuO|HkeTc)7PZ;urdPC+qr%wN{GX;_au`_592rKx8#R^OxU02?$ z4nHjk_z{q!hzS$s``|%P!jHrm4x@9?KdiuCvO^Dm-=I|Qm%G=n5ZYWtzRZ%i`)ZzXUN#N4% z|1U$ZpxI5~-`Rt&94>Lg^2s^VH8y8q&cQ_A>N0Y`jKAZI6aomAAHNvl_$}H2FH-8M zavO#6Rt>gf#>nR>K~~QZ>Kukcu~L+Z+*=^*DLt~)W;q?Dsgoi4a=Y-hh*;k;ZpUS_ zK^B^KMO+ztH(GdAlPOJPW$1VKVW^JL?31+lb$iWR_(hzN3eCyy?_UXdyd6nTB(L#} zm~T4J0r+>JY?$Q9UYOP*)66URZ-%>oc3#>B>3s>_l&rVraIsjgr;|Il** z7L!?EVZTXp)cL58*9P@iKJsWtWa${ z*HZ_RrIY;!quU6hq_U2Oc22Y3TwT)Y7ch}!Vg1vNLT6Gl((X?xWegria15Jx-uRaT zg5M2@rYHnNS&$iphHX&rS477Q%49%-pDEc}a*O*O2|2oK{0y+?Q>s#v#F-i??ChEi1ZC4St-#P{fa0zy6nyMK~SeYLssy^6WcknkPpu zqj-7@P%{+N9v193bbzK=oa<1HEg@`8wRlf^J0f07`om`XnV}6a(Z-W0=P`~0UYO;^ zc#%62U$*R z)zNHr_(e{sbo10G;+jLow92_~l4`=8oo*D92N()m#4Tn*2iX3nbV}NCNAr64a@CJ% zIB}`253Xs9r^X-V%bghA^s+@iMDe|gZYY?nABa?Cug7)Yyz2BEIZKS%9;nS+Pz`=s z)lm_fvswRN%p&-Rb6&J=jdS0ixL_0GFBIrynRsKt%17k%u+vVD5t;$}7B0PPo9|g4 ztTx{;wq4uK@s@R1VXqYa+L<0^^T#mX%h7ds-}X|9sOq3WP2pFu`1eF?C>4MS^imL) zWWqM-XVoS-kkt}?V`m!EGlY;wu^1XuJ>QHrHoN6LS5$RD;KecsFW{)jfKBlAJWHDTA!HL(^n+V|p) zk8NidN3tjEE-uKBE^z|%tDN#G9plAb}8L;x^iOC{0)_XV3Es8=*((fI-X&wK6kc63>b5> z!{ARWlREJu9%2F0F1dD@L!>Lh@b2-B@WYzk^JhDGEH3gr4=E ziae8W%V}=V%6yk(&$)0N<~Ypkodi5Mh(`ICuo~$Z5%4&#Gp&X^A8To5| zmnwa+tIt=t6YgF=u5G04A52*=z51&7Bxfbci5WxH@u5(lrD6?cZqiO#fQK?D7ALmW z4fixc8Z1#1dPGL7Aepe0QZlaodIelhuZVEr$*eZAF1e`oo=!SQuq)uGbOJ z9wFnYQXaT&jdrXNouy$m-p2ozx78Sayi&8Hy;J6M3o#B4^|^zHA-JTUzbuvzD_mEC zYG}{h*nwsWPK+^@F7e)-8v?YwV$&WB4@YV~zExQ;ROet2v+Mc!8Jo{dl20Co)hsD; zfQYfjeEAZLW{gYT?U8Es=L(4e~s#G$&NCy_qSBdN7>Ll}nq(f+tZ zljNK(Q2ar%Qgiu<0kxj3VG;wbQON!CafmT-iN`KW40uSGO0I1X7baBdFkW2QUT7-X#Ro^nTXTowx%mu80D;3x! zdky;Tca#z?(pxM;j0v{yCM9|d_{#3#gC@i(r_QUBU;LJ-=ha~cS_<)|K-CZSh)nO2 z1+B9rx;4;GA-yHxIoXK$defZL zf1q5$n?0~+vZahkDY|w6LbL=2AVT7vf(nxv1og(q2+_f|>+;*~(D>JR&-DnU82CXs z7ja&#O6b*aAn7}}F%!%eAKSh1EP906uq1NLjni7^9U$kHurUfv4}m=^_%GF1B26co zJg|M(%k%Q2O$p-MJ`xU2i+}aWP0rlwb<%LSI)|cqLLHIS&@N{mi<&$02CtLQGy8-- zb%lh;-YH-S$(Q!nnh&k-XYmc7YuCS%U##QZ#;}Q4pXP!HEwauy@EZGkR2S^786J&& zFP~n;C6!m1Fxxmo%Me;eodp{D<^0^yCGM8sJ2l}Ls86SOSoy?2Tj}xK#hcEL;%0f_ z5hN@UmQQ7P7u0B}WZTanbP?yl83)SHRUKK6A*+d3}-^YfXjR zA5hnA_cifgnsm4w&Q|?-YpfNkh)$vBNed`4xrvXs?0a(kJ^SkBf7s}!MW#jUn9Z|n zyk&MH4%cV&VYDOGvfPqyP9hPl0NmC0 zZ(B$yu~k*Q9o?P+Z&rVq56DGO-A7j_oMQ+}3QT96b-Ow(_6T;)3@|AORineM2pJ;x zO!(8Uwv5>ix+g3U%9CFidXR+=g5#5{&1-bGd_+1AGb?^VMFkM{P@DS~&I7+YyZpCL zsS#a5nP>afl!iwIH~27Tg@8jg?2l=Eld?GNoP_k&>ts@E>#y=qm)GN&u4R?$PuSE5 z9&fg&oh&>H2@q9~ERr9b>GFuVOLPyRvXROe+qB)U&5>~F?<-g>iruQuBh2w9=wvT% zePE#MhWokq>$G7~(<%!ezNII7X>N~_woMe5fcL5O?tk|c&G|t8;l1S(_vuH0$&Tk6 z3WZ4(ZfQ7_&fu(#(_PHA^iA6{V|h1@}At~;{M{DMaz3dx<^XB1rYxQO8JNDB%OG)Z`3VUlIIn2hQzMIu#CQE6R7=sYn*_8 zC%|qf@n34ek5j`CdG&ccnl>>btM-*2sG;+^)QVR(>w{*AWlw_S^;JHrNUyp9LYYSE zPh=|GN}IN%>lL_a{IlE#PUwZlbXdlbc(F~XF;}_i`O`kw@g1=Q6D2TvKP~A=6uF6T3zF2hD$N# zeoG((a=Ac0xQpO@dJ-Br^YILZo9&jjZ8*2)PF%u=B9SR=?N8*XNSM^!kx`<>Khr+)=L>o-fh;`>ax60A%P8!+T^ z7Jbqu4kfVjy?SC!tUM+G)a(blF(Dlu zDMtpp)Hqs!is5P>gBnO=a#@*S3;#I)iSDWrKx#g&$6Dxt=>!$6rsXTyWOYv3W@@`mLXF{=1_W%f)gLWcXzv+$U) zB(lpdL{6KX!(ss0HQO>U81=1Ff8#7TBL|+(R^{c`k;-yC{ zkb+FAv2Ca+uDZtJqWNvERc!-byh8i*Q=zVXDvsS12`2M0_ zhN$$b7d|lb4&4dqa=7{bgWbL)aUR@jKz*rCdS#Z=>S%GR(4aBC8%r<5_{P?fLYsZ9 zdgBXT4cdLQ2gooIjz^K!baFCdJXjfQ3tJb>N*7*J-1|c1x-4kKt0wEcJL{$h+R7S; zJr$mHD?f=YIB&{S zyyv)LIk^uWHgJmp)v7sI9mmidgz$a6svMfJI4Z08D+@F^ja+j^1XC}M|H5{RIqRuK z7kHimK8|D-AF2?Vkc2Xt+*MWTE8q4yAwj0|S(KD$Zl(;slDX%vN2BbaVN$8Cp zf8~P!QP8re`oR6;jnC_#qCubQ zh4)5bEP|LGy0Yp+EnKX-|7%bkU4fL<>-5<4_!Da)0t46_AA$1X!_CHNBu#;?*9U!Q zHqq=97e8BdI4y3L?t^C8U-C1yC*jP3M3~(Za|qlU@qWx=GCcNJHJgvtg<2UJT!6CC z?#ce`lVOa%c#jtJ08>7Bu^l5MW?ab0(S=Al8HL|D<`@>yE)kR-*y7%Fz1bUm6p(%& zMVH@K>nT09%8|II6$U3z zlR1D`38?++bT|pAc6}4?G}em}E0#ko@V!G5(Ug}H2X7q5A*e#Ja@!wN#@?vFOJ_*z zRcvFvJ7f)RPIXtN@8Q~JCSI5{WIYF~%Np>=Oj@TEF+U;#sS!(x*BVAiem6iFki?I> zFz-KD{x{CY-29I-Y1>L5=C9vF$n>~!;drlK{Xwa3HPiHJ3+9!FP8mB;g) zJZSi0(Q(Rby9>L+;(9_z4d)_-o-@~;#0sbp8j^-dRz*s3B)XM^&OEIz>?RTMw`=dE z>G8c_`9G2MJ4C^QdFlkTR)>-hdIa!qN@8ksBhuGzosHKocpt-rtSj7K(C7JbZ`49X zYCG`RdUC_|%r}bKAKrzpG8tS%x#O-H&7l7(1gzd3RHpCPP-XZrVEJzH{K|0-ZTR2Z zOz!_cXFJx*MndCCqd;<080km2i!yg(shcCjc_^T!Ngv6sOpwMAW(@xj|MJ;a>LTAm z5fUp+nmi#kTx==)W6U31ST-mxes|}c0riFdDlP7`kdw)8>M^(V6LuHIPx_s$^?K#} ztqEq>GTLye6VKM6GTL4}xa05r!Dj8<#8NoGTUpQjySR;7j}>m9l7#$+vhgrW}^o>9oXn3EV_tcl5N?YSb_dj@U>{T^SpRxCfNH}@!&^Cp_ zQF0hbmx1td?E_D^a!wHwb8K&%QFCe*tBvLQR5|1?;MK1UHmVZocU_&21pi%n z6%#WA3sJ9cetr}@^?sdcbtCCFtN7!x%vwTBwSiO0+$fV>->}1wZVLB-7}2`q>EGct z5>Zg?F63GvcE63*tV%--s9Q}=k-nML^YIO-P&FE()Y;d-V!!2nj@%mJ{M}?T9v~&-kQbD|9TdEXg4{mF$J7_@ zV$UOlIkO1~t$*lu3v{8rv|ZO2?7|oncJysYA*0oqlT4`=_3y!LL_PSRyrP;{AV?T2 zjP*^4z;|N!L5GU(hV{g16$|`fDa(Fx-u}P%vR1+$ApFyN9UqrvJZD0~@EEa@!X_5< z(VG|}7{U>yW_)?Zx8t3v2tfbCJYu|P62VL@e6SXLf!tqN zWxm;5SbJZFKbQMh)^Wa`XqS@?<3pemXIYhTep!u;N5nXVzeB^KoOu3imlGzohMHutTod37}iZa##q$UUHAE^&0QL!2~ zo-t$oz{^5L30>ZeP^LSP3)YH756wCE@w^eYqqA|JE?j6N(f?eYz?Sy3rThw`S_8c3 zK=EZjqqUKg(eP0INF3I4^?;rHjd7&JFwuXEGOC&4b0E7{J*M=?rTUdDkpFHc{^^H1 z=YX_TExPY`-R@YB@@f|g!Zor0i2XIDF>2#|>2~X6lCnk-C~7zH3!kvd$F0{Bh1ZkH zt&x|rpqqqIl1Iy;pzFG!>!!D?G*hg%j{>*EFLH`e(Pf5_qprlzpNU*n!_MTj+s!syb zB3NpdO6*T1X)w|PVGv8d1$C2|FG;}~ ztmGw0$>YE(gpRJg-F7f+NajNO6AJ3p!zc6w%X(-U5zI+5-W@@7GYIpS^3u5uHQbxB zZ6H{@Yd9ygYT3Y_*TElkkhiT-Z|T#*0W1!4Rv`S&(+2Rp~O~Zh+t9^Ikp8%!tAoq;dc4B8y)Rdle);fQ(Gn&La z$>S>XAcy=y6x?xckAnP{v*UU6^Tg-l=WWbGiJUso7HX4I)PdrCTU|%AbBeL;6E3(O zF=Sp+$BBWcWJnsAKaJn|?UE{R3{5?MJhi&)%t776Kvkr)@(Av!NkP(lF_CLvCHy!r z;RVr;=r?k6bE^(wNE)O55Y9Dbq;;bD?J<~6@`y~wnS*$)w$N}WAe4Xmh&!vm zP!p?YHYP;AOeo-pcM)`4{f>2&_cxpVYM1KANuQLIqu(2Sfj-XDXc(U zcZj^P*T&O>V#%x0GAzT0wy&?b8)#e2>0$4;DdxOR!lDol=fP@h$2i`U4-;8k?s6^Y zcpA-R5J3aKzHaEm*n#EXTu8ssxw_RCWLvYgaPhb9sINz8;@r9o7hYe5#dn;(($3k9 z;JGqs-;@k@!d!c|P8 zb&(G988&l^AxPG1rhOu}xImhq*e#5j|Hu`y%V{Q!3e(iqEpFAAyg`+FzP*36GCoU3 z%BLd?F3EWvL=(WvtP_JoDjqJ(cvFo|Kd0)($Hz@D*%CAvJ$@&)vgFwW^(tA9F2Hv5eSoJ|9_!oqlCrS7_xBe1*cAY}*gjQ^rnQp2&Z1Kn5m;|qTBmY>yavWtKx zgwL$XR6a^Af>MfDqeS6(T1^0dOeM=H4@~+on7MC0&Yxc5QR$JO%UCbv0f$dPoS3~g z(x%4|5c$zhGtbkA8=1eQ6JlT4^Mt15AC(|$FgIGBX`IV+5G{X}`M69?+n&PxKJbj&F><7D zjNwQ*oa4(bS6XI9BTCWUY--o6Vj!^kwg#in9X&>S8Y?M47FQb)qsTb;-LvAo3kEd! zNMJZYQY&E~2;_rCmDB`an~G`*DC*V}_1s4%C{6EiO_H7O=A-!IM9cM~kRP69Nx|FBpQ#f?PVs+yqF-#UN?hd~QI1)W<~$w+59 zP{HUAj=E#XDrAc(bPjwMcPN+N6IT|Q7V0?N;;h^@k4W_YX-9~d@2V7Gwr14;XqV7A zUpra~x*6NniGvCLmGf-heu^TsBlzB_nEk|+`Ak=U6#8Gw zt*aAHD<6$)p>i!vlih?b9OV-l4!)c@7oGi_qA*+@_fE6MjOQU7NTAbK+=8dT3CH1q z@_~qR_86I=KBR(O4YUfUdR>kKZptW&ExIK;8G)in!Rx3j#qzhsJ$v>gset)DW0o`~K&0l>nx%G0Q)X zy{K76-DXsFq=f3PY>FnRD_L^4Z>QY7*k4n0=Fk2(PwF5lZSN6n-QN`Bp{LBhWq*_q zm7W&#C%}q#Mk6eeBw;0FSD34ox zS^${t{{(T=MXH3rqqt$j!9DyDtAC?JCk3;At7877(H6k$VfRDn!=i>Ujc3P2%}RdR zLw^~Vg(khbim{`=&@8|)<9;Oh+sbM0_f5k`l*P3{@9kw-`sinb)XBHY0@OFYVdSDu zF`SUByM18-v@ssSinjb5R@#wpq+>EKkGEVAj13Jlj=INB{<;E+x^qSI{QLaoVxneW zkF|hFpmxCTkK1*#P6vv~#QVmJJ92k6-I+t5n~OlEW9r|2pw?=QCr&N&$hEjLYFl+b z87I+pMP!AJ^n~hY*Hzi10?^xtT|*2PX^(F`r@xoXZSzfl2-sdkUjmYKr0LxeB8}*9 z!$l3*&)ZZ}x6~Hfs6YK$r|g&@uzb1z?6NOYi;U}q8!u=t@}qmf8GLLl!Dl#`-q>n# zxMPF1tZNQ(J&DEn$YI5uDIvT;Gi)E#jS$Oxcjsmgdcc{{@oL8=xoJ9U+G=cu7 z-CfEfY%Hh5GqCDIpM5k@VWRj&c`fTjY}d(oLU4*kB)dc}n)ZG3PpMz(yMOfQinR0x zNNJai^NHX3y9-Sew#oOa&^HOsc8omF2+BWJ89>8gNvm<2l(roGQL~VY`o8>%Ec(QB z5QK8)f7J#uqE5&Pj&t{IUY@CyQD)jDXq8-&lEvx2v+`=7!1)s@NErw}ngU?*ME|#i~edpqhcS^N%wrGzsX~SzJf0w>Le|AJ#nsibHD7%LVJ_`>Gfa#I->27 zW>S{ckD-c!^JUzMpdr*z3!zpdzv@FQ1X-Zcq`L!4eEQ7Q5(CB(5~qHxC#cNU-l*01^JAmt zk`$OfzbUFTPoV<;^Zu;TY=2@B)sZ%qy=XjG>B}&}ejnep97ER#iC~WFu4|cohCIa9sB zvg|BrF#6z3nEd3n<_@6FdUo-^;aE>e%-vwYT9E#;G|sZn{h6+N%Kei*drN)kXPwQf zC4$^vuPR0}DzgvtI_?6Hjzrz#wgS!sl5SD#pe}P0GxjfZVA| zQmCTHSFk-D!X)Q5ews*_^SoUD#7sh|CIVO?TuDj+emgG8WuYLS6P@1!v*I;UoiQgr zGz_thV@a9Lqd#j*>yf*Szz5A6?CAJ*t}Iu25Xfsjbeh+D2!pDR8|_2^%SR5FZMC5j zX%8`3)=MYcC0rfd4SiT`xy{iv``g81E&66TiL1_7Dzr2YyMn%B;x62q62e(O7`A|^ zssgfqFSY`STW$Td6HZ-3%ugiD>1t~MQ!1(CZ!lKG$aH*@4g=ofD4;y5<`MR#q~?1| zDEdz%L|ygk7YRO4zGke-OuA?PqYJvNEY+Wdk@{9xLXnNrV@cUexiSQjmPVzvjU&CS zodq(Ul~DgJZ#GHlzeEuKcJIoXChiSeU+MJqE#c;VRN%nm(xAekUH{nVK?dQKxmDjr z(rR|I3K+jxF`XQ4FY*)NhK2}u^kPNT!KHmYVEFx}c&mfib%WnaIJkVicqwWpoQm^b zAz8G1RL>diZYvIZ%}n-)MT>l@zMWuME8W1dg@|1{TehW*su>k;%Um{nNZlDl3VBS7 z;yJ-BnrHyI_+T3jDbdfw3JLJ~b31uTvOR9IdS zn_b`G!^4Wb)G*4L2cZ*QVNv+eSLvc3rVLgpOj|U3PEjkY+lo7NPa2ez0p?E|)p4P) ztFc9iPj>Ks&Jkor^$3GU6iLOW^=Xg(Qi9E@1J5TBxFteZ@#&EoiHc!xj&S2=qnb7FU+hY&Z%_x z=5XqF>cU#7^MK|tV&g?jgC>OZ-u*i(I$J~oO6qwUEs6E^zx_T z&sWu6{W;js$sUs}N=;#+)qZn<;>&nmH2*(UQqm?%B)XQ)Rz(J!*#%a2hBfS*ZrQH! ztnV+GnVHnBEs){H!JzfT-5W_IV&fKjuLE0c+3TDrMcm4i0 z4IJyXx*$m@Y%8#~?V=oi{wDDebo2#;ovG?df zi!)C-SgdBFs)-TQ<#zP4$R;xOe7>5NyPW!8uE?JmpIEB&!O9YK%C)1cDe#T&lq(P9 zj|7V_R{`34Yu7NZLRoZ}T63yg+gGB-16_lOx`;@kh zH~L9&iBe`{WoDM_;Qigr_^JfYht+ z?FU2oZ((mxd7s|2{}FVv0dB;Yvj}xeK+d3i(j5h!1dA5S5jRso*x7qa+>Sm@PPO}F zXy&U26r_`tv-RzgS%=qXK^D&|O!D=4Yc0o!w?&$U1Ql=bXn6DtHE#P?SFDP^9$emC z7xTI&gePBbsxyk=BWiK|0hUU9Tq@+u`uwJz67qM~B4+Pf+d zebHKz+|en!vK>)@E^95s87^&sj23|9Iya<@A$`!`^Z2=S9vm zFBY46olMGSqH9__$@pWD}dJl+RFy zk6q%Xtx${5=idolqe=?gj`TVK0gGJsu@P5iMq)%>8c!qnMD)OqB2Imv&h!c zz3?GVDE|A5`@Ra21XGbf%%Ak$K{_xg+R&D`%^P-`_9zdp6wmv z@VW2$x(&#c+KSoGMTz91ZQQk{x!&*B=H7uqZxjiv(7S0!epPw-`$h%-K0#|!NuMxl zZY?Tw&J6SKY+2G&2pxx^cpl}}>-5HU^E0V-;Rfo=rZ^#pzx&umYcfuWuGDkJZ^YP? zu7sV%60|$v$}Cc4Hp4&zz2c|%sim)Vs2T@yy|=O;ejVSiEZR>9JW>YIcwPh#3K<7v z|34R?Glw3Pv^T4u9j62Idu?LguF{5sE57y_z7WCHZBMb;gE4BK%}f8-;QKYwwI1hl z$kT4h-!R;bTz*z7t~-!j7+p|7a`>A8)K*)@+{JHPU(i|USc{Aj7(Gb`L1J=X1#Fke zt_M#6q8ihp1LKIE>JbmbPLo7(KJ1>|kJW&Q&u(+G+SRM#={sg1r_G!e^Ns*XnjAG4 zC@ubdz0cOIr4iuax%qHz%;j=VEDvh!tA$xb&<%8$#j>4mRNG87E)z}7J$EfBN1ZAz ziyi-awc09nc@g7|skm@2^K^+O`?GsHM!>nTf3i8)QM)FsENWtNELtu0H2;;d;obHi zub;$)gw$ev9(B@OFD`kF3nggI&d$ziK&_A7GVVK;`TiXHUNvS0G~v_tZ8y&ed)8J8n5;>RVi^#Qzh;(rX1N;AI9(*&ST8PWx{Xn5b^I0KQp`iO&t*y z7QYZd)9N_&Z5wO?&+O~s^T#hOj;WR+#^nDQhia||Xn!n+w>WM(G!#fO`Tj8fK=0%9 z^8B>*z{hJW?qNg^4z|!VVZ4bbcYWh=Q}zp`RrQ~#;@;Y8084|aq0UIu?*3Rs@W-)y zb8`H3%M7$EpX0E-pZxK6$Kp2gLr(vM^55Ce$HTa<{mKevzCJ3_FkB&Z4Bbt8VUT~6 z6?8E7Jil5tJ^CKNQ!y3&0nmT+)3C!hCga* zF(|6ew6gLUrM4e(#kIA!x4)w-G>=>%=)~e$UiPR_FJLP?hjN54lskXfihbxt2+W=2 zA1(Vh@LG8Ls+cBR!+$V-s3bWM@=Cf-bZ)Ts0!Qb(e1>&m8yCuD1z*_0J2xpMe2v%| znY#0Q8K-q!#*=kVw9>*O|GA7xHvy93WrWXm%B#5`XixKjc#XhP(e(ya)y(?M`5f6S z<#C~dqs#w})nD_zp7j5~ujX6+!Z(VlwL^Ux(ZTvdLM!8tw|AY4R$%Q@L_14m(=oX} zwB>coDDnj`x2ReeqXlQphpx>`ID+rKs=cNFGZxubkyQP`*tkp$uQW>NzNUF(&|#=P zDg$fmr3v(xpJ~rJ#wO$4)^VTb4%zEIBDjo0C`*GEV7fSwydh3NWvnMUz9VH)<}{LM zpX=nH*!9;_kLieq`PEzg9CHd#&w0xFYU$@6D))8gTYUNr1;JShPIBxGLZIqV&h&xx z22oGeKO%uwNAeY8x+QXhQc|?b=gcjhIpYw5#HHNT(mR=LRIcl4tA+)ypZA(%isb5F zZyYMcL=m2M@2#*b*Bc+?ZV2Mssl22fFDB3jQ(Sd-fG`&;loD%v>}$JFioUzqqgK~5 zhM;hBnMf8jx%dit=XsF{r!8rh)QWQ2!>!>H{%!Ie9i>)9D;Koez~&?uX0}#}`itBN ze<9NEcpa|!C5P}=$*CX=62&eS-l z&2y#)S5m$7k?_v1x4x1tb*07S`^rVrEbIddz@SPl64!?kIKK1T@As@(ES?ulWlXg-;4sqPC`WOmSP=RU7=DOFWkO-@e6T4t23~t4* z?=k*QI};>zZ~djfoAWjy>JXQQH#o4KWY>K^|JZPI9so?+omavzFowTlU=7-*)Hr?r zn&w=vqK*p)&xMfrP2JoE^Im;=^Ib0p%VI8^@p&;Cq4R`uN%VpjH-?v_K7v&#;qPZT zzJVrf$}xQiah8#m<2CyJn3BU~kg=w6srOPY&8-U@k{oe1j-|cqlXx;jZVw1$=7W6@_3i8PPE>yDq<6w^OaVQ)v79QDC_r|J&$i59>bV z!^ap_9%CMJdgVbt0qreu*dm(U_{28etMS5oN{pKnKI>(7;aArJIO|iY_dP57imu&s zt$xi9mCAa+L04DiG(lkM0>ADKG3RO$ppGQ$&FX2bWj4XPH*AumN3q_eKOmI)^S8N< zpF7ghE2Ehr%Uf-0Jtd%6T$grf0c7 z@P+Y?7>iBLX5q#O$iqymh6~n1mI#~TGB7IbLx0OY)`mxSKw2l}IcEtglI?NP@mwYF z?qk-n`3N=|`+>dozo~On{PJgt4_#9!k71RKqikRDtVUe+?i&Y1-3u^>wL0KMjP($jp)k59S;v{=TP$W)Vba{ zz$pAI>`rV*dZ%m6#MWtFZFdyY$aquM#em)!H38J#!Ceb&w9C-GONDWvspzqX0B)U^C+MRtkh&LubjcLL8#m4MtRXgkN{; zd%Xo54?@mroZif&Z`267foAIe^P{(4Nfhe&+q`s~;x zA4lKMDPvO?n4pV1+%rb&OTI%rtw`Z)ecWYF!i@>$ENay#g(Y+}4 zqDMXo(~cU@_z~m5CBm7cOa8^kQmBpIEOHiu+0e-e;`+wYiOW6~swibTqs(^PP?0d$ zAb)&kRYDfSa=VTl0BwDz>3+#rgG2JxS2v|(m1(KI`$p^x3hu+&(hFO##Fp@~2A0T0 zlhhO}%27&~{rGxtRJ90{Gt?N&s(GtVURmp{rv25{QVCJNXEcvGo1Q;cay<-ABv;y* zz^wlGqR6yHJ*PnL2o7rZZ!jc5gtCghcXxNin;6UBf;M{DIR}SG6sfD#;=qQnvd0$p zT9-3mS5^%JXcqAgC>7)KR*$RyRQPT4Y2DiDEGiA#IqrjVoWMl8P{iFxX@nic7>PZA zFUwzr)Zw9LUDpPT-ZsyZ#GB~_W)Os0A0rqzefDn;{FLC8ZCbT?in((P~UqKBYm+a{&Do0W+` zXqk3K80xWG{}IDt_otp~C&tP|U7V}F51Sas)1|l}3_gTk2lm(X6Z_gEQIb4+50uM% zlkOi@s=P{7ACoTu2IwL&RJwoc#YyQ5VtxA;10d=w&)q$I_IaZN7)ERh)X_W!_y$@h zgfs_v-$LLh+5)uF0ONiGrtoFzt}s$wfu3#n7vk2fnl9l#7MpEBaM{f12>XrX6DTY8=!EnPxOSv2Z1#t>+KsdKiefPVPE&E-aAX4)hCZ@d0238uD&*@#My``E>ma7xc>t3NBj77gyc+vC0_5jSX$xi73 zbZu@9=;tS7D{7G3Hs^0 zkx;VUMZkgswV4UBjG&^2DP4eM6_=a*e(!hFLa_LQ5m&5vI=8}F6N2Qo`sBT(N9yg< z=aQjOgY_a}&GSaVqRbPS;wEE6>HVUeQu5#fM2uk1!{OqTP5DW67UEsU=!qMfAy&p4hhMkF;55#0+_!33a*EdO~E14Dk;FIBm zoq9xlxdjXGMCQJ`z1=wMTaXv{aL6jFE*sFy>g~D0Es%VB63FM8em?+@{8NG0Y$ac6Sya#xmYG!Z1%0(~I@bHGcHH!limGCB{gb zxRYz{)kl7_al56@za`?a(_?M~+9pX1S%s-pQ8;C`#J z%-Xy06Q%GX-Q8)MH&o)sY19_=7lK-8{aqjGdO+ujs{u>4w>6w>QlJ3p*?r1o8@Ac% ze{*L6y+#BWRb(qSc`U8OjSIbUkn!>fc=4+A(r@#6WGb;(^q<hV;$H;7MqoT1O-*)P6 zVvOYb#rE??^tuU?L&4u$e$ClGnn_HR_Zd&!%kCF=QB)y)sXsg&-*o}yQ@L??gg}+l zY7~BKS6bjABL9M>;UjF|$l?w0XE0YaM(z0tnCCr5XQVIk2_v<^I;F^S+CTlF*I}ja z?0utnBdj!!B8L3Ax*$k=qy_=8m6V_{Apr-O_p?FYk-|x%RuldrrPI`5t zx!rZXJaq1%yxCi;rnv-_jq=#CTa4qUsZH~5CA5qlZ9)fyJSO$W?`3wv0Qf2V13xL* zj?ULI%#83*eVTR7pf>&;wHBF$xXpSbu~AXoC_`Lg<=0cSTc2jlOV8l&UsXjsDploG zff~(K)zD47R`0tXqXOx^g2_on8x3gJ8((3#=1A42y*_2wIX+7(fd-yBp9DxY>XEM^ z{Ui^e9Vyduc>h4<-%=`-AFMCSeNmS((<{ds0=QI2dWXUT0grT7umzvbMXv#$VrwwnQQn`m-}hQOCeV#Q%F9BZ>KyeM?E{g4087Q2iuLbs zPW-6_^NW;NCxzzXn^%Fv_(Mh7q}AHY*5~)XM9rd4YVo-SIg~Q&A9kXGvoJ^fcwpJK zxyDqAZS^}a*F5`d3j?ywKimQl-K7Bvyf-6XiZOf1RU9|9QXl2ls$-r&W=t7QH!j{c zWhBvG=lm-kTn445+dCnWm5S(6`N252TTUpVRp_Jw4|Ixpd+cdu2RDI)RVJ|qonqf# z9zgVcpIqAYj=<*#|J$M;_V)Id1z0#3YYW%TTcD#t(LtPMSCF0R9xK*S(?C`(Zj}Ok zl}ihSgoAQLnZj5i6ZBo`Rx7hh-!xwhF^4yh-y#vdOS)%~77i8e0gE{t$S%z-T<`ra$_(C)_qu5fX%(uE zs%@2ulKRwwin84E4SueAKXx_JCE5&*Fb@w(;WXRx-n%z|`+M07qczCUTdj5!luDJ+ z7G3Yhj8s8GY%Ht*_lq5Kc-KV4`Pr@`qT;70|K>ryjv1u6(2)cY*(=P_5ky?I=&J1T3TF#?e*zR$r@SpXXLl z)pAdaf9+!Y>EJu&FOGSb-yAw<{oANufVba?CZlj6$VbgPUBMkbcO0Vw(JUG0jSWZn zLglro3#T4T2amtG`Q#{SP;EwgTSl{wpw+vty*K&yXVgq$yy5M^p8S|un?H9(509(c za$T4jgp*`}`rAN1H|Ov7#a-Y(?b7wk7N7l{@Y8B|)r=|YF)hbrJc@EHYlk$GjatT5v|c#_G5es?sP zO~mIho0e4aMYk0WK2^TuCR=rL;~fELGAR*|Wj&!^JzR3JOg^b)o087+FtiVW>~m4$z*htbJn)TI3{H7kZ)-3kD@_96g^WJf(+BaRgUzq z-~GKJoUQMGtl%xi>T_;wKfQFU;=hiF)!k~9jb$6N2%EP^@d93+whS{gDh)O>>UDPQ z&O*u{5lZBwR9JhYvdm$wjnz!jh=!@bn9 zv0#AkH#90PF9h<5i0JqL;vK0cm2@G+WRKWAzw3LN5(B86o53hP)<*KKHM;hC1@s;1 zcMCXJd79d22DNH6;TViwn^Y>9nl3GE$fxbIHGC@TL=8AYi^OP$R?iWa`4JH63uFcue_Cag)VnqE03@#PWo(l#+x2Ysf| zXC=CnW1-vW1{0-z<)5?LxN;I1a7*p>Z0kV%vV5Gr(s_H)s|9se&1cwWQy>p~oI|1G z<*B+jto9q_^&N?aVWy?yKWqh=_$X+<@MGW;)rsk0>~J)jYyVW7I(o}z%JTVo%Tw(E zr~6g(8*;r?L_>bMQde1jIcd#F&v!I&W>uoE{Pc;3@l1xw?Sk&FP3GQ{`bf$O9T}pS z$UV7Wa=*pW15s4=d#%Rf*62}*n2JP9FQxYa!SN=-SQaCzj}yWyI1l^{okM#)+W;XQ z^jc9-1Dli?!S+QvKmWAib4y7j z1Z3i$UmwdA*=&n!T3n8Y5HyvvP83>8c32&z+cFJM-3Juf+3=<)hij1Ik_yHGSkJqB zR!BBD8W%j-{bk)HnryVR^;0e4scY0YthHB8OQFX853WUS@tGw=$?*H&varGQ1r-U0 z6Mdbzdum5YXrjfc)Ke%R%~gQUY>f9c8-LJ9u3{}J8}oramUfm+ZolQtX?}t-f~>l? zT954)p1UAX*v4vi^z*3jY2@!E5etIPiJe3<|yx z{h(5n3a4>MyOnCCq@=9wd?Ntf1?lOaV;wqw?K{~1QGf@cbZ7=e4{f_b{QnvEmZCAQ z1O-W!&378vtj^fXLn8F~J{WPZr(AcB+Q-P*6fP*#iPs5TBY#+5Jnn9 zkUN6wlP+Od3)EI*)klwcYDHLDO;^%5z0Pg|xx-S7so#;&bR+=|7HLG*@f9YN#%IUd z#Mon+6G*j96RIvCd5$wdU1{eV*p;3ID%7xkdQ zoA&T~yX&=9+KyLrUQ-Fl_c*T&M%Wt)-~+%WMwGNIM~D#>e1~X8b4I{_27^x#gHn?jX%#Z~lKI`Ih4+SXW{U-w38*L1We9y$Zg-s&BtNbL*bbCMgs z0*_OFINX6vUtI8>Dtwy!xBcBueHyxsw?3g&fo1l|i<0uGA|ojn1IGjCoaJhI<0Uc1 zQ2ZRt&RJg@$ft;Wgq?o(sX@t0Fx&Ho)z~+W^6`Z6rrd8s!pw!cDo-=ZVT{(rR2Q7B zkJDb$ML2i+Fc>UG1Q8z;BcjaJ^k6sSIM}E2uA9&?bwi&7m6y@LtQ z4Zo&j%PG(H+vF=SAj3c~y|=O9V#UkaXzsRIsr?(%8FoK>U^n5kaHmBRPs8P_l2!9Z z-F;zpT)Vl%d}y+U55aTSL@CE+f;`tWzc8yqvC<`5E}`Bx*0Tdhk?+%yKDj6HgIx;F z0J-Rt!HB}SB3|OQv4u#4;Zl^&EY(!~Nd9+aDMb}0FVWzGJLN`i8sVX<# zNRXL1X|tt5NfSXeogsUVS5keaO>FLK8@~(MEbkT1{9@DEH!K5o6jtnbI%-4AdC0L= zin>?O^AplVt<>Gg)+yp z4AwVOwV|9n>HkuE~R+E^G0 zz1IBCFuvy5h%<7}S=C@!Y9u5F-E}jt58$mBsctu|o-^HWG23pSal+(B?+zTI;l1%* zpFQq0eoH)9nN@#nxZjK9$kxDwF-N(r{{!B%k$7Mcx@^S{-qQKAL-zQdC7d>sM=gHe z`YM};HGd!*_p!i8jI*Z-}qLXcC! z7?aZOHW<})oPIuv=4)D=`VP-m2;Da`Z!nMMX|&l*sZWJCPG+b~#RNR_Op;BWB#pzM zT(1cfeJf}hPIOUd^lBLYlB0l-{*+_P{VON? zB`p2x$K?l6iZhUO3LTo6c#FBI$D=ycMd0{1D#YQ=+smuZoqy-(+DZg-D@e1fwqho;J}9H$DckPD_EheJ zJu~hmc|JEg5eL6kv71Cvh%(Us6crnhEk-%>+aiM!N!>- zjJU)@>gbaYE&xx)ck)_?$`2xXrjwFND*&V^9;uhS1z}-f{)yFS6lcZ^enq+wCze9X z+|Dzfw zV|J$VJ1@40Ul45`5ALU}6mWOeIA8Ndm&I!PFNFs&8rKC+C>Ylp=O{FJN2JsD}iH}>_Wj=CE>iftjS?HAy`4|nJY(W2C-Afovns*9Q}5h)9&(j1!wNNNAj1mH6DZS z>oO@``mTPfQQ6NOJ3apUkecBSARW?59^L(z1r)bO-v0Ar>(tQS7MWjhtoXe`BZrK8 zvrgMh;snf|pm&xKzyW$gB<<`9HdEi2Y0RWon6ar?dqN#duus9uMbWH!0O z<9p;Qp#Ajbt+DRAZHP9E_RRS~HyJ!a_8o#K7kWW4CN{RvA2e6)9q3IBeDvfmrF;~m z5f+|LM+45}sI}5}sue?X@gt-v`=0I3w{T;lr};RBlJ75`?U?sK5sKGQmz;{hLVb$x zXcZ$H#Dw#)1y_Ez)bP7>#eMO4v@CgbTPzPWH7rTp0ODT9Y!srkQ)&~-yIBut-*ePq ztBAmClhRq}ewgEDowsU^bXpVmM;VCI$T*1nWu6`PHhM5Ux;G_uh-aL-YW9ey`eKkVTyIA`f=d!wrJy8~ahnUdVYEbUX*~ zc>?Fg(K09RctF8ijug+<={4kyO>;Sc_w-8HUpLX`M}ED@{U~y>DTDYuvs0OG-@bkQ z`c*g7XPL_X`DV&UeDfFEw(y-F&3S<-k@c9!)5xv9&U{G8p($w$Ht8;g)` z{|2*!zP`!px!rDe_Q7Lhb|S{7LA}f|&+8*u`&-M-)f;ME)iIcn zni_BDEfu_%K5-Q;r{)McB3JU7-dlU)pT;%*6;_>vZ3<4B8;$IVupOynQT%Y(tKZ|Z zT#49WweC%s61u=STYHzt;LSMcs~oYpS)SgyO7%Oi-CezHB+gCYpO}9-Y^ZfH_2Qoh znW&eUg(hZE(7%1{(mNe}ei3#i?yXyFz5!Qj%kB$S{BIjI{aah7H%#gHpr?2}sXDnI zn_8#pL1aPQ_(6Y-zuAN|*TmmM%ge+z!SUdjx1+C_eQIRKjni^CLM!s5*{?nMWF7<_ zj`@lB$MejTc#ADLPW*02mdE=ydGZPV3J57*O8NHf8@|g%R^JPKbEQc-JJZko@ut*% z5SIlly0uZ*9TwbABRnQ*g&Tx(?|2r&U?oj|X^QdF@fFxioJe6Ui_cASz(N)p4Jwt2 zE=1O}2vuo41G%@pyH5QMkwISlI35q(o(-ORH2vG8CICUz&bQl*%%a;xfaz24)4h7k zIuQ#GQp%w^Bsh_;xj{Cy4}TED0uqnjtW9vIn8p-;H8FcMz5+4pAX9zQ6at$^x_$&k zHCkO5mBkO%-wX*;cOiST%yEzb_jEx{+*((vV(iwZd*q#^pJP|dG=61u7mcQzju4KA zm)G+CI3efw$7PUv^NNYQQnr;d*M!E%HF4kZd6$r>9z+)HQnbYK6a}*~KLTELdnEeB zT)wN1>`V!UxWyE_;I9rjz)Vz^Y+SEbq0yipIpy}<;Dt#p?5WGn-f#=}_@2YMjKiGJ;>P*E%A2W%O8lzu&Ld>#twGa5Kgjn(cai=RQJYgtAKMgB2w5>@kZ$ z3~kihh*C99sD95EU;vx*6R|ohHYPG3#vSDKm$$dKx-BIbLdcwim&ZX)+53R{1<8Vo zJvg|m@zhI|K}a23gcDhKQ&g~d*U4OC_V9O**)K-c57Ox?X3OO<=>_)5u0e|%9rvSo zy+&wbnMZ;+{d&EA|Ni}Qxo9tlEw-O zdOT>>!>MDIupd+xYs6VakDzh=$oT5%;*luaw33d`b+QgE4JJ^Q$|HJJPH)C_N2Bi7 zgeR~Y;!{)h6c<0u@^Ssz$b+0?Ku)QBC)?2NcI&_v#8{IHd#>Zncg|yYo^~;T<4$&e z6M;&|fseBI!KSA~_!NHfS71}Qh~f>GH?@XN$RdWlc-|jB!KP!zll!`JUugA>9UW_9 zdE6p$(^k{z4u5yjoBvb_<_S61~f62QvJx2`3=)xWHM!~7Y^Q|a?q?C9k)eq`|JHLo|{jKy7zx({xNM8 zU_i?7*h`i7h(9bcYp6bbg|&NJUr+7}^6vbfiblk4X3fEVAAAQEB0P6Z^h=-3LHpzJ zpx0$A!oRc4?D1Gl_0y&J&9Fi4%> zOnImx^zrq2-Pu!-dP+0bbo8_-v?v!;;yQX)*E?|4N3!{?bugF6VKdy25bvpBtzg44SD|hGmNSbm8Y}xfyAE&2~^v!LG5rbvNc+m=}zH9UGwi&3dNj zINou<{_-MsKvr`y#X7x4oHa;Z|MiGR*E#m@!%1kL9zQt#W`4-ZI>z{szQCI@C>M5p zXBK!?KJn%x&FiuboTZevx3{;qx69>nzuzyH%iG)A^?EI(TrL+d!iB}M{GV^8#F5&J zZ;x-=R(Cg`i^;53S&C!Y%eKOE{CHEwi7Yd5#|?qR`zw~|2zO8*k6_En&C0WQXeGZ! z7!&Uobb73HpK6Q9LtC8p_O&dsA=%TEPvASdOVu3@BRwwgSN)6UNlyi@ei3i?gUC}R z*X~Z)NBcc!%ba1P2=C*8tpfZj$a6a0!S`@oJu++5zUH#_yS69DJB;Jyimbdneq|~3 z&b(HZs4q~3j=|SD@Q$XjrD%Mn%jG6uFqX#6E8<+>tT05*@!IuzmXN0ef1DP&x$ev3 zbrOHpJ1K#Gk>f(n=7iYVBCa&%zM4HQVv_1__9H^O`SY$|o<{X}au9nW4g*68z_CVi zN%#5m7}a-2}MZZohv0 zsykKHvpih_@8HzAb_1X3#tD>4R#oZT@i7vo0RJqRuZi7{OETTlp2%@s#%=ptoZY*T z#q*@4@e|kI?1Ms!KJ^1ToS?fYQxXqw4k|lH^)7V~EzKHs_ze`K2jOcac%37%hXKSx zMwk`1gK`x-dxEybFDlL)1&}p^o6{e8gpd1WIv0KkUQt2rI(dCLg(t9iDnE=9mp+v@YwbX~lh=Hhg>&EG zGD7rmC4>e$MG05sXZ>rg&DUeG7oY2RwQ8kOiq|fgr9KR(FKct0rIgF%Qg4DZ-y7ZX zar@8OjV$fzu8)wZfbN7s|NXd|NGYcBY7x3y4~AV;INPsAIBqen>3F-^1tXk8ishhy zS>Z%AV{8a``c6t< z+pXrnpngsWJfz(~FY#=L=T9Lwae|n&3rxzY&tYghfvnHP<(5W#(RIj2pHIEX!o|~0LyA&s}JVn8*j8I4UZbjC8 zywB7nj%vJq{gWRl$ESs_*XyTGpZL0rc&0}n>2A1aqFU^ByM26o+-|peHz##BDyCKY zZq&c-j~@`8MZ6W_^~hjDi|%!CVDY`Z4c?!kHwu^iLHtSEFb*DH14 z*YYcKe*bHQUi#eAD9*`Lj2?}6et)TawW)j_Kf`}IZ{kO2JU+QM@kTpRO;LMoblBnP zUMt7CjBVTM9vLIvuf&%gn=@WPm&>J;^7->;?y^2z9`@bn*2Lpn;Bf#%3q;h`&enbK zhq~M2Y;so99nrrXH?jD+vGs7?Q8+8l!ln(FHApwMdbiuG5FybUSIeu3EKt-w3!Y3I zdCzkx-`S2EPvI%jX~(R#zK*E%()Ler^0H^i08$B8p_?;|DHQj2A%k8+;+#{*Y^t0R}`N2Hy#aor>8s1d-1Ur~{qsi&jlK1y_ zzF$VY#V`dJU%y=Ksm&tn|NHmvU%!6+`t^$&xufaC+KuK6NuR>slk5QJVMxukH8%QL z*mFX0S1E?!mGRoG0kl89r!#WJGM&eIb<-kjxUY$5Me!_MM&6Mg(QD8STpb*+I1%v| zLfL*qGM}AzqZA@*y>RddkxfK?=e$N{XZjxKSz>R6!G6l|crqR{9r;p7tHDf z=ATskr}3ZmZ#J#UiaOPX&*0h2j0miJD6)NQ}~e}86=H4`#l%4o_%)yti&=VlV~y!Av35h0uR`p*Iv8EA*Z22#b9)ZY4&~?1p9AVd zR32B4|F_8(=qH|E`SEg$2Qg;c8vG9NlpdbNFqgoL^9p!qyA565v}8xdd674R;wTmJ zYho5V?P;Ix^S-8uP|)u{JD1}+it5?154#Es<5_5gn*MqboEvbp&>fi+f!W?y$M|)9 zdEB;x?NTP3n+-U&tPI8;|BJMs+&F8OIK)S>Z9D9CCIxuFs4%NFQ+>1k7%CpmI7imF zO{c#8va?|*=;Qw%@cnq_KenN`I;~ew=j5?e-XOWYWc@_j3Os@Q3M8;Kzet+8TXE)G zRFD~CM0H(84d%K{My&NBgyP=xU|>{5z>P?+<53v&=Tb^JX1%;2Iyj4WoZ?|NWq$6a zdM}<4M!31ZdZl1L4z4dpn?fFI6w7?fFyC?2_)T3#@a$}#eC<2>X&w*ajbz$i=asq8 zk%DTZ+QInoc(teJQ`&!MVv9UB^l>jX;`q@AD+Mdsnz0=_x0^uwco_IFg9KqmyMy?3 zo!!wM{Og&h?$^-Wu4zM09-)zTDH5g(cHD;eSfhb)zXZhd$#%r^+uK{6=1x(EWgFHN`;NKqSV4nMQ{W?}T;dbUtn1LkZ%K4aeCf7DZ=3}0YO;>jCiB%!N zvpv4YR4I;pGk&JS^YT?Qw1iXkS9&f*z?wOvWY!%Za<-3<+?-nAvtqhJ32M_cSFh~| z8ydDsDfI&-+64igiSa;mKRyq-F&6~on|7-&0o`u5`~7~oTaWn~0g%xN>x>5&k}f6x|Q zM$2<4z))J>nXeScWFJHZ`Oa27c=3tEZFf9(`{noV-?XK6FgB!kQO^v?M=2S%8-gJo zk4MPE@5O_8yxZ4(_`!Fig*}D5sXmzXudrdtUx>^pE)R0tV#0b{MxAuP<2d;S_`Cof zGb7a3?g$s{BD2qqSuco(Q?o6zUo0XIGdPy<7#orMIIm>7P2%HTjOqNQeH!eeu`&De zsW=WksFOeBU(_GXe6@$UE7+Ar%i=6iLok;!V^4F>IGR>>%oOb^FNuExnYg*v#yM0D zs#o~+QzrCvRqtwlcXIMNGVf>U`01dk!tdX|KR!NaYAsfCbbd1hnHACMDSp<7J_v^T z7k+#N_i)mu6UuQbYvOzFQ!tOnxR1`nKfc|Dsms=irEz*t3S3w6X%wH zX*uP`b$ny*j_t|y#&PZ%MAbnPF?c?TZ+j%L+Q&V@GoX^SHEAosNlb6fIG-k7>>}*E2$PQu+dtHlB zU&o+z8O?eY5g0$)#D?U|+s`DWzO4mw(o7bkq`aB7RV0it#uY;iplxLtRlr zMi|F0bQ!FKI}4QcL9fCo2%GqIyZ2)&<3JH@Nl&J;PEcng60-7?-o&|hqTuCgVxXuc{;|rzX;#=z3!xziie;;v!3a5q%7yI zso5!xJ#Y?Y%Gz#-JVn8*JV@rXH?=YnFx|XTDW#M`%P~HE`oz~|cz`*7%3Y5DmAE&@ zgJ(M3Zns~*e*OOad)2-h{LYi$=D`lmYwI{Yh=7T_BCK*fcxU@%nArI9QgeR|Pu`CE ze5dVp$t~$wm>&^1O<6?I$i5q!dgIrWIml*V?$GYoTgL;xG>y>F++h6hca=R^M8%?b z7H$=?7c*Y`Y{x8w3@h*lR^>JDCOpiskH~sk7wHy-9NR(5-YGg(r&C8fY>#Htx@f&P zr?ii^?zKxX5X)Zqfb+$iTIOZqdOTNl^Hf7#{x{>zlk2%V+g8vloaM+^85==|A2JpP z8Eg^7`(JQf;$CoPZ)?Z(YmU?(&mTBi9RvIVgUfpCIEOb4BFw0NdNDQ<|7PbJ;XAvq zEC)5`!{%OZ<~$@e9g;}ra$!bpg!<_IS@8yPfmz6ckU3baZ=W3qmENDyo2StL(bO@} zS5S2{-~KZw9M2rU&@$`v0r_sl%DzXm`}mm@QX_-yiQyIeajZu{!- z;x-$fDOnuU`A`>bhDu$wqmOHOA~!4NfkA~4c?5=c<=GcwDIVKAXlrMWh5286#22wa z0`=eE`7p0-+{i(EBKl@c3i?iN>c-NPXVs$S10;C$>ws_0(*&w`{?=4;5nnn7GYPy3 z>gHbS@fxx1xWi@YqUVZ(2=~XeTCq&QFv~|gx=1$TmX`#1JRhS=4>0!Pp0YpQ1<-Nj z9dF8K^1wCwzIV(?DmB+-;Gq)Fln`uqogJLRFw?0pwMnz$!Ps|0L~0<@5y2~&xMQvQ zv?ZCBZ}KxqU=!A(hee`1&b@{}In_}16mjBQY~Ekss*S!#&>o!2`^?o%rY=ES1^#A= zC-cZ}%Dj(kj=3n|c*pzXfNT4R^%_C_RW&MEoU1`3_wuoAS48y(^>nl~*pE_u2egS- z1|jWGlT)?vI9Swfh~jim_l~$l?3v|#WX4R#JSoeA9MYVP(R*ZIp5erodkE9zpq+7ZjgXh?JUo5nwFOe~ zFi|h?+zs&i6t45+)_d!RFEfelKNo)rBpyrW;9s3*rr3dw@XJ^)J}c2jSmP}-rc=|N zOWXBiiq$w=`Q9aqsQmo-b9$xX?uN+11)gCq<T;#t-Pz6%@qz?RM*~6Q9qe>I{5Wi-|gUHg47* z^!K?L=>;=6SKqT|a}Ca`(IPvWu^qg%ab^w_v_H;Vx2~%lWb6n?m&r!8?whe3kT>25 z?$saLJOHpC)ZG>ytKM1h%K0hrHkS}2WYt-E&EjH>hKX$!&cKFV)HOL(drt1!kA2F| z^bz8hu@HVH1u<4ftL=C=UCYpx8QbxiNi#>q@)Z0jKBZlR_>5g+*sYyhr_=mzrii(M z;QR4}WSsNl(sBC9*<9GlG@~$Sy;dRZn%`2{)D_O^D^;P9XajD*gtF zCqI|R8gZ*?V{R;I<1)hax!h>9ZbGj?qrzIQgv;6v<6}(_a&x&bV6G2HNuDI$^i9000o2NklIL?x!5KpwSK~21NW390Z+ZhB zl{IDkEOZNp>{R@W)2@i%k!AKHhs=sJ{&cF({(YLB)>cO=9o*|{*?tQ4Sr5aa$Q0zY z!tv0!i8yqY43gf50*`-j#ooa|*iF-!*o2O;+>FB!rOpY(K_a_Vl|O&}T<>epzw5?# zWFBs!<=9d3~pAZj;eK+E}9;8Wq3%$zxxTvZVE^s$)vpkK+UOfo+^!3(~ zZQ=gvCa*Vc>m9_?`W0@&K7Y-{+5M&fLKOco2870A6_-cI>%53B5c0|+`Gn*Ge;bJ7 z_w)+T(wK+$7mbInMe57%%{+|crS5h`2HPJeG(qmd*I^Cf1n~>b%QR)Z_GIFlQz`#W zbOniEnWr^P@cv(KHQ;S#%{PIMc9yUaNIww<4$d&L`Hr|FW#spX}3 zvYUwpQ4wd2J@aObclCk#T9jAVOZ(hitanjr&X{6?kOr>Krkc)LSK0TLmeh%q8 zvo!Mu`Js}TL&nuSjV9V{hp$(goMX$%9{!?s%n{xU^uqhy=Epz~OCvTxdMHv}*N;$O zb0LcpJMIh)#udIoeqx4PL9y>dUOJ7(ov3MZPhMgF=J?C)8O!k%jdv~{vCr}*-mgvh z6=-Msg71%GQ!OCm`b6z=xqSNci4;L%fKxVq;<}9ZZaN;5L`wHCtPYaN`)+Kb!$KMPMD+2V5bLI}O^$;q9e2|1$c%JwC^1nh;>_%p3RtWO$vBs;%eB@BrV9G};T?Uu=Pf=%p zlXKk%b^S;^)I;gSUCngGhAE={=3K~Z+Or9rOWmhgf3q!+7xrlbM7tN~rmu;|*6?Di z!?5)+ICtpQT3@***NBba93Ovm3~RYO?%HEw5HjL|%jNP4&zUrDUSD`Iu&un4SdKj% z_G9w4t67}qLB643eTrNjQ&1cLg1qB{6a*^L9W+g}f2~kl*n)jZ;PGUf|FiD~Xj5;b zsR6Vl_Z!E3A8Si8FW=-R5Z^DrRCXkW?gwp;Qz%|JDC4~2l&0lJaBfHJ@dal5xGBeY z0`h#Zj%Q7zZ`)r#itqbJlE}T@ z@i^xD;@>0PkR}3I38@hDU0**+k+%cZoo%RI)IK2NvxHePrxcPtb8RXlxO^tpx{DRF zT3;_nJbQM~IW{rd(o+TML3~>T%D^#5s!8YlrwS2& zjRYh})lhZXR+n-&{VVLxcU8OJ@7L?~_wV0l-dC!Mm}3nXOWC$okoo*Tx7+b%LyB^1 zH;TGNME$Ti>IgZ7vRLp_u^c~Schs&bvP=|7ktn8KDqv+NbZgn@)y;w`=%%F z{+<357k=@cO=X?M%bQwzvW~HAyIDM@U-o_1mZAJ0kb9mrn+`bD*)8)H`EmZ2UbHg8 z`A@{Zv8+cQ5eE9W)>k-P*edW>(3`xcoxxL3oa>Q72an)ErX2YU0vAP0I}xHwBO z`2F!pTjz?G|eUAEa{lxGR^1${FN@3%Lt4nCDv|m+Wi|yTq|8b93SitBYZVc-;3L& zgWPo5R;Dcax!8DO+(b<;dpe5aqRd<*^|^s=u1RKvfz9{hPH-iB4Z>1K4DyYQ^nskJ zlix4zmSZRmN~F5r-~?CI+r*KUnvFC-9~2=B~|PtN&g=}f1L zaITb=N4SZzIr26Grs>crX5p__1o1XY)^PyDXeYgMxx_w|Bs z+Wb2*gVdo>oDY{0w-O!kFf4^MbKed9g^)_qT$_sm=c}#sa*Oi<$rkucIC&sqY0b|NLBh#e(xXANd_I6tbB!FR|D|Zq{X#>2yeY4yR9mw$3$$=7UCV9*^Y|bw`(IS3vE1|C3~DrQS!UCbWOO zc*q{F^6hq`=KvFZYI4tX+LglQ=JB{IdHRBVWD$Ygf&UcGsk~6^&~ZOzV;VqtT)V`H zuafJIgre3-iP@p0PjNQ!4Chz(;-)2X^@gWE*-o%_NIeqlqs z#%+3j@cM-pdm=Wn_SforI7O~*YDBa>?(U(n?T=#(7^`uaDRJf5-1AGH?C}&%ZQDMg zWsH3l#)0IO>65$Z&i0q-j*WmLt6yVvI^vV$>>R8ahd9?tahWnRS*bQnLhRhWP%(K4 zzlit#{?1E2i|Z&d_3EC_&n0g+4YT#7^Np(s!jxmGCCyPMmPd$Jl9f*&dg!#eYKUxN8|>{=dHw8Aj0EAcDBGgfdLOD(|LPvMxKFiT%*Yh7 zUJt^L*Pk4J(_*54nK!-Tbu^`CBU{Fa2lWBh;2M=jDY}0q3I+QI1*OoD@>iJQaD8{cX>D8|*_r41j%+9$?A;F*JnmDz` zwT@n4EjME<=E7W_Jx%k*F5t2r$30(|d~j;LA5;z+&jYW_E9@1!Z*Z51*^%J7-*Jw& zrGqNF2M0ghi^K8RyM6&A8?i{gGi9*z(m^U2?7q?LlH=U`2whVzLfce-7Kyw;O$X!M zcI@8j&4EX#{QKYko>dpENWpphjp(I(mewZGD))&P%!n~%lb6SZ=;MUr5%O*=M#|E@ z@8YA%SqePjJ>*^qBu5IK^4scL%wtV>#BhhF#sUzq)3|>^2jtl~p;I(UTOC+cULmH4ye>7x!G4p-E_@4LBnt^lJ#EOXw0 z-|NDf_;FKi#`rLXxg(J ziI*zboQ@gCCsl5~O2*|~t;h>{S>U@_ye}Id`x9xxfss?V$MsJklZAyGj0@gAYurBO zaS%_p{mq87F$3Al+n#+Y@qYu=s#&_@XBLn1RfSq*eSb~p`#O9(){X^n#fj$=Gv~~u z0JcX#K3s=W(Ea$VByY~s!s`*SjPt}dQv_m8JWF;E;xi-b4bXY=Vi)+LqTx+4uYxG8UJl3Liiud%ZRx%jS- zAYm)+UqtqN0h%R9uvzl@g?Bub1J27#q@cMGJp46szRvs!q3SHA> zDPElS$qRe=qNuk9OgY7=6)tmAr|Rj@y7G^YkHOeZ`Dm~z9ZOAW+K}5kw!iRu96wog zN-4M7jUUnGn$ni`&fWUX!ft(Sp&2K!JTa&clbU}8s}Z>`?)Q7$P=oJr;DvNb7RI0D z#8kUDFYJ!n>u@u`g1@S4Zc^qQH?`JfJb3(w-Y;XDO1e=*2Duo}O!5mtk3c&E%JT-_0?LqHA;fp5@x?BfY@O?b;3`6AZEU2Lf9k6oK7Tc}l-4$0NibwgVr5 z4S97AavfIzH4&P!Hdnl^P~-HT-)g?^`~CRb07s&sQd_2-Q?3Esx3{hh%$Myw^CIKj z+_|N&voNnP_0FDYA#*K$Vpcwrd+nOoM6Avl=wmLzm?Av(qv#7d-nh(LL5?@uHSM_{ z)R;=R00000NkvXXu0mjfKTSH4 literal 0 HcmV?d00001 diff --git a/doc/manual/en/images/keycloak-password-update.png b/doc/manual/en/images/keycloak-password-update.png new file mode 100644 index 0000000000000000000000000000000000000000..49b49151b16da7efc73c5cd4afdeff4fb87afe7f GIT binary patch literal 236238 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

>r?srvtAdHMTODR3;F3- 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 z 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/dev/tcp/127.0.0.1/8080 || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 60s + volumes: valkey-data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4de25b2d..08cf028b 100644 --- a/pom.xml +++ b/pom.xml @@ -131,11 +131,20 @@ rest-assured test + + io.quarkus + quarkus-oidc + io.quarkus quarkus-jdbc-h2 test + + io.quarkus + quarkus-test-security-jwt + test + org.webjars.npm chart.js diff --git a/src/backend/main/java/ai/mnemosyne_systems/model/User.java b/src/backend/main/java/ai/mnemosyne_systems/model/User.java index 5ed886b9..7b8684a9 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/model/User.java +++ b/src/backend/main/java/ai/mnemosyne_systems/model/User.java @@ -40,6 +40,9 @@ public class User extends PanacheEntityBase { @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq") public Long id; + @Column(name = "keycloak_id", unique = true, nullable = true) + public String keycloakId; + @Column(nullable = false) public String name; @@ -69,7 +72,7 @@ public class User extends PanacheEntityBase { @Column(name = "user_type", nullable = false) public String type; - @Column(name = "password_hash", nullable = false) + @Column(name = "password_hash") public String passwordHash; @Column(name = "logo_base64", columnDefinition = "text") diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/AdminUserApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/AdminUserApiResource.java index ebbdabd5..ff8c91b2 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/AdminUserApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/AdminUserApiResource.java @@ -12,8 +12,8 @@ import ai.mnemosyne_systems.model.Country; import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; +import jakarta.annotation.security.RolesAllowed; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; @@ -25,13 +25,13 @@ @Path("/api/admin/users") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("admin") public class AdminUserApiResource { @GET @Transactional - public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam("authUserIdV3") String auth, - @QueryParam("companyId") Long companyId) { - OwnerResource.requireAdmin(auth); + public UserDirectoryApiModels.DirectoryListResponse list(@QueryParam("companyId") Long companyId) { + List companies = Company.list("order by name"); boolean unassignedSelected = companyId != null && companyId.longValue() == 0L; Company selectedCompany = unassignedSelected ? null : selectCompany(companies, companyId); @@ -52,10 +52,9 @@ public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam("authUserI @GET @Path("/bootstrap") @Transactional - public UserDirectoryApiModels.UserFormResponse bootstrap(@CookieParam("authUserIdV3") String auth, - @QueryParam("userId") Long userId, @QueryParam("companyId") Long companyId, - @QueryParam("countryId") Long countryId) { - OwnerResource.requireAdmin(auth); + public UserDirectoryApiModels.UserFormResponse bootstrap(@QueryParam("userId") Long userId, + @QueryParam("companyId") Long companyId, @QueryParam("countryId") Long countryId) { + List companies = Company.list("order by name"); User user = userId == null ? new User() : User.findById(userId); if (userId != null && user == null) { @@ -102,9 +101,8 @@ public UserDirectoryApiModels.UserFormResponse bootstrap(@CookieParam("authUserI @GET @Path("/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse detail(@CookieParam("authUserIdV3") String auth, - @PathParam("id") Long id) { - OwnerResource.requireAdmin(auth); + public UserDirectoryApiModels.UserDetailResponse detail(@PathParam("id") Long id) { + User user = User.findById(id); if (user == null) { throw new NotFoundException(); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/AppSessionResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/AppSessionResource.java index e81d5e10..a826c483 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/AppSessionResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/AppSessionResource.java @@ -11,23 +11,29 @@ import ai.mnemosyne_systems.infra.BrandingProvider; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import jakarta.inject.Inject; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import java.util.List; +import jakarta.annotation.security.PermitAll; @Path("/api/app/session") @Produces(MediaType.APPLICATION_JSON) +@PermitAll public class AppSessionResource { @Inject BrandingProvider brandingProvider; + @Inject + CurrentUser currentUser; + @GET - public SessionResponse session(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { + public SessionResponse session() { + String installationCompanyName = brandingProvider.installationCompanyName(); String installationLogoBase64 = brandingProvider.installationLogoBase64(); String installationBackgroundBase64 = brandingProvider.installationBackgroundBase64(); @@ -47,7 +53,10 @@ public SessionResponse session(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) String installationTamRoleColor = brandingProvider.installationTamRoleColor(); String installationUserRoleColor = brandingProvider.installationUserRoleColor(); String installationExternalRoleColor = brandingProvider.installationExternalRoleColor(); - User user = AuthHelper.findUser(auth); + + // resolve user from Keycloak + User user = currentUser.getOrNull(); + if (user == null) { return new SessionResponse(false, null, null, null, null, null, installationCompanyName, installationLogoBase64, installationBackgroundBase64, installationHeaderFooterColor, diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleApiResource.java index bd332106..6822c346 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleApiResource.java @@ -12,9 +12,11 @@ import ai.mnemosyne_systems.model.Attachment; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; @@ -22,9 +24,7 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -32,14 +32,18 @@ @Path("/api/articles") @Produces(MediaType.APPLICATION_JSON) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class ArticleApiResource { + @Inject + CurrentUser currentUser; + @GET @Transactional - public ArticleListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryParam("page") Integer page, - @QueryParam("pageSize") Integer pageSize, @QueryParam("sort") String sort, @QueryParam("dir") String dir, - @QueryParam("q") String q) { - User user = requireUser(auth); + public ArticleListResponse list(@QueryParam("page") Integer page, @QueryParam("pageSize") Integer pageSize, + @QueryParam("sort") String sort, @QueryParam("dir") String dir, @QueryParam("q") String q) { + User user = currentUser.get(); + ArticleResource.ensureSampleArticle(); String needle = q == null ? "" : q.trim().toLowerCase(Locale.ROOT); List allItems = Article.

list("order by lower(title), id").stream() @@ -59,17 +63,15 @@ public ArticleListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("/bootstrap") - public ArticleBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - User user = requireUser(auth); + public ArticleBootstrapResponse bootstrap() { + User user = currentUser.get(); return new ArticleBootstrapResponse(ArticleResource.canEdit(user), AuthHelper.isAdmin(user)); } @GET @Path("/suggest") @Transactional - public ArticleSuggestionResponse suggest(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("q") @DefaultValue("") String q) { - requireUser(auth); + public ArticleSuggestionResponse suggest(@QueryParam("q") @DefaultValue("") String q) { ArticleResource.ensureSampleArticle(); String needle = q == null ? "" : q.trim().toLowerCase(Locale.ROOT); List
all = Article.
list("order by id desc"); @@ -92,8 +94,8 @@ public ArticleSuggestionResponse suggest(@CookieParam(AuthHelper.AUTH_COOKIE) St @GET @Path("/{id}") @Transactional - public ArticleDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public ArticleDetailResponse detail(@PathParam("id") Long id) { + User user = currentUser.get(); ArticleResource.ensureSampleArticle(); Article article = ArticleResource.findArticleWithAttachments(id); if (article == null) { @@ -105,14 +107,6 @@ public ArticleDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String ArticleResource.canEdit(user) ? "/articles/" + article.id + "/edit" : null, attachments); } - private User requireUser(String auth) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - private ArticleAttachment toAttachmentResponse(Attachment attachment) { return new ArticleAttachment(attachment.id, attachment.name, attachment.mimeType, attachment.sizeLabel(), "/attachments/" + attachment.id + "/data"); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleResource.java index 89337148..0ba8abba 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/ArticleResource.java @@ -13,12 +13,14 @@ import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AttachmentHelper; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.quarkus.hibernate.orm.panache.Panache; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.NotFoundException; @@ -40,6 +42,7 @@ @Path("/articles") @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class ArticleResource { @jakarta.inject.Inject ai.mnemosyne_systems.service.EventService eventService; @@ -49,25 +52,28 @@ public class ArticleResource { private static final String SAMPLE_BODY = "## Welcome to billetsys\n\n- Open a ticket from the Tickets menu\n- Use Markdown in messages\n- Attach files with the attachment picker"; private static final String LEGACY_SAMPLE_BODY_PREFIX = "# Getting Started"; + @Inject + CurrentUser currentUser; + @GET @Transactional - public Response list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireLoggedIn(auth); + public Response list() { + currentUser.get(); ensureSampleArticle(); return Response.seeOther(URI.create("/articles")).build(); } @GET @Path("/create") - public Response createForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireCreateEdit(auth); + public Response createForm() { + requireCreateEdit(); return Response.seeOther(URI.create("/articles/new")).build(); } @GET @Path("/{id}/edit") - public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireCreateEdit(auth); + public Response editForm(@PathParam("id") Long id) { + requireCreateEdit(); if (findArticleWithAttachments(id) == null) { throw new NotFoundException(); } @@ -76,8 +82,8 @@ public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path @GET @Path("/{id}") - public Response view(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireView(auth); + public Response view(@PathParam("id") Long id) { + requireView(); ensureSampleArticle(); if (findArticleWithAttachments(id) == null) { throw new NotFoundException(); @@ -88,9 +94,9 @@ public Response view(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathPara @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { - requireCreateEdit(auth); + public Response create(@HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { + requireCreateEdit(); + User user = currentUser.get(); String title = AttachmentHelper.readFormValue(input, "title"); String tags = AttachmentHelper.readFormValue(input, "tags"); String body = AttachmentHelper.readFormValue(input, "body"); @@ -100,8 +106,8 @@ public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, article.tags = tags == null ? null : tags.trim(); article.body = body.trim(); article.persist(); - eventService.record(article.id, ai.mnemosyne_systems.model.event.EventConstants.ARTICLE_CREATED, null, - AuthHelper.findUser(auth).id, "Article created"); + eventService.record(article.id, ai.mnemosyne_systems.model.event.EventConstants.ARTICLE_CREATED, null, user.id, + "Article created"); List attachments = storeAttachments(article, AttachmentHelper.readAttachments(input, "attachments")); article.body = resolveInlineAttachmentUrls(article.body, attachments); @@ -112,9 +118,9 @@ public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path("/{id}") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { - requireCreateEdit(auth); + public Response update(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + MultipartFormDataInput input) { + requireCreateEdit(); Article article = Article .find("select distinct a from Article a left join fetch a.attachments where a.id = ?1", id) .firstResult(); @@ -137,15 +143,15 @@ public Response update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathPa @POST @Path("/{id}/delete") @Transactional - public Response delete(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { - requireAdmin(auth); + @RolesAllowed("admin") + public Response delete(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { Article article = Article.findById(id); if (article == null) { throw new NotFoundException(); } - eventService.record(article.id, ai.mnemosyne_systems.model.event.EventConstants.ARTICLE_DELETED, null, - AuthHelper.findUser(auth).id, "Article deleted"); + User user = currentUser.get(); + eventService.record(article.id, ai.mnemosyne_systems.model.event.EventConstants.ARTICLE_DELETED, null, user.id, + "Article deleted"); article.delete(); return ReactRedirectSupport.redirect(client, "/articles"); } @@ -227,28 +233,29 @@ static Article findArticleWithAttachments(Long id) { .firstResult(); } - private User requireLoggedIn(String auth) { - User user = AuthHelper.findUser(auth); + private User requireLoggedIn() { + User user = currentUser.get(); if (user == null) { throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); } return user; } - private User requireView(String auth) { - return requireLoggedIn(auth); + private User requireView() { + return requireLoggedIn(); } - private User requireCreateEdit(String auth) { - User user = requireLoggedIn(auth); + private User requireCreateEdit() { + User user = currentUser.get(); if (!canEdit(user)) { + // TODO return ForbiddenException() ?! throw new WebApplicationException(Response.seeOther(URI.create("/articles")).build()); } return user; } - private User requireAdmin(String auth) { - User user = requireLoggedIn(auth); + private User requireAdmin() { + User user = requireLoggedIn(); if (!AuthHelper.isAdmin(user)) { throw new WebApplicationException(Response.seeOther(URI.create("/articles")).build()); } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentApiResource.java index cecff1ae..988a3cf3 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentApiResource.java @@ -13,9 +13,12 @@ import ai.mnemosyne_systems.model.Ticket; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; @@ -32,16 +35,18 @@ @Path("/api/attachments") @Produces(MediaType.APPLICATION_JSON) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class AttachmentApiResource { + @Inject + CurrentUser currentUser; + @GET @Path("/{id}") @Transactional - public AttachmentResponse view(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } + public AttachmentResponse view(@PathParam("id") Long id) { + User user = currentUser.get(); + Attachment attachment = Attachment.findById(id); if (attachment == null) { throw new NotFoundException(); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentResource.java index 07f01a06..6c4be013 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/AttachmentResource.java @@ -13,14 +13,15 @@ import ai.mnemosyne_systems.model.Ticket; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; -import jakarta.ws.rs.CookieParam; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.net.URI; @@ -29,16 +30,17 @@ @Path("/attachments") @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class AttachmentResource { + @Inject + CurrentUser currentUser; + @GET @Path("/{id}/data") @Produces("*/*") - public Response data(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } + public Response data(@PathParam("id") Long id) { + User user = currentUser.get(); Attachment attachment = Attachment.findById(id); if (attachment == null) { throw new NotFoundException(); @@ -57,11 +59,9 @@ public Response data(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathPara @GET @Path("/{id}") - public Response view(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } + public Response view(@PathParam("id") Long id) { + User user = currentUser.get(); + Attachment attachment = Attachment.findById(id); if (attachment == null) { throw new NotFoundException(); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/AuthResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/AuthResource.java index cafb363a..af29e101 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/AuthResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/AuthResource.java @@ -6,6 +6,7 @@ * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. */ +// We don't need it anymore. package ai.mnemosyne_systems.resource; import ai.mnemosyne_systems.model.User; diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryApiResource.java index 93b4611d..e67b7b6a 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryApiResource.java @@ -12,9 +12,12 @@ import ai.mnemosyne_systems.model.Category; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; @@ -28,12 +31,15 @@ @Path("/api/categories") @Produces(MediaType.APPLICATION_JSON) @Blocking +@RolesAllowed("admin") public class CategoryApiResource { + @Inject + CurrentUser currentUser; + @GET @Transactional - public CategoryListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public CategoryListResponse list() { List items = Category . list("order by name asc").stream().map(category -> new CategorySummary(category.id, category.name, CategoryResource.firstLinePlainText(category.description), category.isDefault)) @@ -43,16 +49,14 @@ public CategoryListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String aut @GET @Path("/bootstrap") - public CategoryBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public CategoryBootstrapResponse bootstrap() { return new CategoryBootstrapResponse(true); } @GET @Path("/{id}") @Transactional - public CategoryDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public CategoryDetailResponse detail(@PathParam("id") Long id) { Category category = CategoryResource.findCategoryWithAttachments(id); if (category == null) { throw new NotFoundException(); @@ -62,14 +66,6 @@ public CategoryDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String "/categories/" + category.id + "/edit", attachments); } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - private CategoryAttachment toAttachmentResponse(Attachment attachment) { return new CategoryAttachment(attachment.id, attachment.name, attachment.mimeType, attachment.sizeLabel(), "/attachments/" + attachment.id + "/data"); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryResource.java index cca419fa..178727f6 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/CategoryResource.java @@ -13,12 +13,15 @@ import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AttachmentHelper; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.quarkus.hibernate.orm.panache.Panache; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.NotFoundException; @@ -39,27 +42,28 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed("admin") public class CategoryResource { @jakarta.inject.Inject ai.mnemosyne_systems.service.EventService eventService; + @Inject + CurrentUser currentUser; + @GET - public Response list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response list() { return Response.seeOther(URI.create("/categories")).build(); } @GET @Path("/create") - public Response createForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response createForm() { return Response.seeOther(URI.create("/categories/new")).build(); } @GET @Path("/{id}/edit") - public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response editForm(@PathParam("id") Long id) { if (findCategoryWithAttachments(id) == null) { throw new NotFoundException(); } @@ -68,8 +72,7 @@ public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path @GET @Path("/{id}") - public Response view(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response view(@PathParam("id") Long id) { if (findCategoryWithAttachments(id) == null) { throw new NotFoundException(); } @@ -79,9 +82,7 @@ public Response view(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathPara @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { - requireAdmin(auth); + public Response create(@HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { String name = AttachmentHelper.readFormValue(input, "name"); String description = AttachmentHelper.readFormValue(input, "description"); String isDefault = AttachmentHelper.readFormValue(input, "isDefault"); @@ -97,8 +98,9 @@ public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, category.description = description == null ? "" : description.trim(); category.isDefault = makeDefault; category.persist(); + User user = currentUser.get(); eventService.record(category.id, ai.mnemosyne_systems.model.event.EventConstants.CATEGORY_CREATED, null, - AuthHelper.findUser(auth).id, "Category created"); + user.id, "Category created"); List attachments = storeAttachments(category, AttachmentHelper.readAttachments(input, "attachments")); category.description = resolveInlineAttachmentUrls(category.description, attachments); @@ -109,9 +111,8 @@ public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path("/{id}") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { - requireAdmin(auth); + public Response update(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + MultipartFormDataInput input) { Category category = Category .find("select distinct c from Category c left join fetch c.attachments where c.id = ?1", id) .firstResult(); @@ -158,16 +159,15 @@ static String firstLinePlainText(String description) { @POST @Path("/{id}/delete") @Transactional - public Response delete(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { - requireAdmin(auth); + public Response delete(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { Category category = Category.findById(id); if (category == null) { throw new NotFoundException(); } Attachment.delete("category", category); + User user = currentUser.get(); eventService.record(category.id, ai.mnemosyne_systems.model.event.EventConstants.CATEGORY_DELETED, null, - AuthHelper.findUser(auth).id, "Category deleted"); + user.id, "Category deleted"); category.delete(); return ReactRedirectSupport.redirect(client, "/categories"); } @@ -210,11 +210,4 @@ private void clearDefaults() { } } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyApiResource.java index e8269fe3..8f588a3e 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyApiResource.java @@ -15,30 +15,27 @@ import ai.mnemosyne_systems.model.Level; import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; -import ai.mnemosyne_systems.util.AuthHelper; +import jakarta.annotation.security.RolesAllowed; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.time.LocalDate; -import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @Path("/api/companies") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("admin") public class CompanyApiResource { @GET @Transactional - public CompanyListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public CompanyListResponse list() { List companies = Company.list("lower(name) <> lower(?1) order by name", "mnemosyne systems"); return new CompanyListResponse("/companies/new", companies.stream().map(this::toSummary).toList()); } @@ -46,8 +43,7 @@ public CompanyListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("/bootstrap") @Transactional - public CompanyBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public CompanyBootstrapResponse bootstrap() { return new CompanyBootstrapResponse(optionCountries(), optionTimezones(), optionUsers(User.TYPE_SUPERUSER, null), optionUsers(User.TYPE_USER, null), optionUsers(User.TYPE_TAM, null), optionEntitlements(), optionLevels(), defaultCountryId(), @@ -59,8 +55,7 @@ public CompanyBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) S @GET @Path("/{id}") @Transactional - public CompanyDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public CompanyDetailResponse detail(@PathParam("id") Long id) { Company company = Company.find("select distinct c from Company c left join fetch c.users where c.id = ?1", id) .firstResult(); if (company == null) { @@ -82,7 +77,7 @@ public CompanyDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String selectedTamOptions.stream().map(UserOption::id).toList()); List superuserOptions = optionUsers(User.TYPE_SUPERUSER, company.id); List userOptions = optionUsers(User.TYPE_USER, company.id); - CompanyBootstrapResponse bootstrap = bootstrap(auth); + CompanyBootstrapResponse bootstrap = bootstrap(); return new CompanyDetailResponse(company.id, company.name, company.address1, company.address2, company.city, company.state, company.zip, company.phoneNumber, company.country == null ? null : company.country.id, company.country == null ? null : company.country.name, @@ -185,14 +180,6 @@ private boolean isEntitlementExpired(CompanyEntitlement entitlement) { return LocalDate.now().isAfter(endDate); } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - public record CompanyListResponse(String createPath, List items) { } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyResource.java index d2d6fcb1..6e41024a 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/CompanyResource.java @@ -17,12 +17,15 @@ import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -44,27 +47,28 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed("admin") public class CompanyResource { + @Inject + CurrentUser currentUser; + @jakarta.inject.Inject ai.mnemosyne_systems.service.EventService eventService; @GET - public Response listCompanies(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response listCompanies() { return Response.seeOther(URI.create("/companies")).build(); } @GET @Path("/create") - public Response createForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response createForm() { return Response.seeOther(URI.create("/companies/new")).build(); } @GET @Path("{id}/edit") - public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response editForm(@PathParam("id") Long id) { if (Company.findById(id) == null) { throw new NotFoundException(); } @@ -73,8 +77,7 @@ public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path @GET @Path("{id}") - public Response viewCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response viewCompany(@PathParam("id") Long id) { if (Company.findById(id) == null) { throw new NotFoundException(); } @@ -84,8 +87,7 @@ public Response viewCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @P @POST @Path("") @Transactional - public Response createCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, + public Response createCompany(@HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, @FormParam("address1") String address1, @FormParam("address2") String address2, @FormParam("city") String city, @FormParam("state") String state, @FormParam("zip") String zip, @FormParam("countryId") Long countryId, @FormParam("timezoneId") Long timezoneId, @@ -95,7 +97,6 @@ public Response createCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @FormParam("entitlementDates") java.util.List entitlementDates, @FormParam("entitlementDurations") java.util.List entitlementDurations, @FormParam("phoneNumber") String phoneNumber) { - requireAdmin(auth); if (name == null || name.isBlank()) { throw new BadRequestException("Name is required"); } @@ -111,8 +112,9 @@ public Response createCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, company.users = resolveUsers(userIds, tamIds, null); company.phoneNumber = phoneNumber; company.persist(); + User user = currentUser.get(); eventService.record(company.id, ai.mnemosyne_systems.model.event.EventConstants.COMPANY_CREATED, company.id, - AuthHelper.findUser(auth).id, "Company created"); + user.id, "Company created"); applyEntitlements(company, entitlementIds, levelIds, entitlementDates, entitlementDurations, java.util.List.of()); return ReactRedirectSupport.redirect(client, "/companies"); @@ -121,18 +123,17 @@ public Response createCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @POST @Path("{id}") @Transactional - public Response updateCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, - @FormParam("address1") String address1, @FormParam("address2") String address2, - @FormParam("city") String city, @FormParam("state") String state, @FormParam("zip") String zip, - @FormParam("countryId") Long countryId, @FormParam("timezoneId") Long timezoneId, - @FormParam("userIds") java.util.List userIds, @FormParam("tamIds") java.util.List tamIds, + public Response updateCompany(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + @FormParam("name") String name, @FormParam("address1") String address1, + @FormParam("address2") String address2, @FormParam("city") String city, @FormParam("state") String state, + @FormParam("zip") String zip, @FormParam("countryId") Long countryId, + @FormParam("timezoneId") Long timezoneId, @FormParam("userIds") java.util.List userIds, + @FormParam("tamIds") java.util.List tamIds, @FormParam("entitlementIds") java.util.List entitlementIds, @FormParam("levelIds") java.util.List levelIds, @FormParam("entitlementDates") java.util.List entitlementDates, @FormParam("entitlementDurations") java.util.List entitlementDurations, @FormParam("phoneNumber") String phoneNumber, @Context HttpServletRequest request) { - requireAdmin(auth); Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); @@ -168,8 +169,9 @@ public Response updateCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, if (Ticket.count("companyEntitlement", entry) > 0) { continue; } + User user = currentUser.get(); eventService.record(entry.id, ai.mnemosyne_systems.model.event.EventConstants.COMPANY_ENTITLEMENT_DELETED, - company.id, AuthHelper.findUser(auth).id, "Company entitlement deleted"); + company.id, user.id, "Company entitlement deleted"); entry.delete(); } return ReactRedirectSupport.redirect(client, "/companies"); @@ -178,27 +180,18 @@ public Response updateCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @POST @Path("{id}/delete") @Transactional - public Response deleteCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { - requireAdmin(auth); + public Response deleteCompany(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); } + User user = currentUser.get(); eventService.record(company.id, ai.mnemosyne_systems.model.event.EventConstants.COMPANY_DELETED, company.id, - AuthHelper.findUser(auth).id, "Company deleted"); + user.id, "Company deleted"); company.delete(); return ReactRedirectSupport.redirect(client, "/companies"); } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - private java.util.List resolveUsers(java.util.List userIds, java.util.List tamIds, Long companyId) { java.util.LinkedHashSet ids = new java.util.LinkedHashSet<>(); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementApiResource.java index c7148bc5..546b67f5 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementApiResource.java @@ -13,8 +13,9 @@ import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.model.Version; import ai.mnemosyne_systems.util.AuthHelper; +import jakarta.annotation.security.RolesAllowed; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.Path; @@ -27,12 +28,12 @@ @Path("/api/entitlements") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("admin") public class EntitlementApiResource { @GET @Transactional - public EntitlementListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public EntitlementListResponse list() { List entitlements = Entitlement .find("select distinct e from Entitlement e left join fetch e.supportLevels").list(); return new EntitlementListResponse("/entitlements/new", entitlements.stream().map(this::toListItem).toList()); @@ -41,17 +42,14 @@ public EntitlementListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String @GET @Path("/bootstrap") @Transactional - public EntitlementBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public EntitlementBootstrapResponse bootstrap() { return bootstrapResponse(); } @GET @Path("/{id}") @Transactional - public EntitlementDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireAdmin(auth); + public EntitlementDetailResponse detail(@PathParam("id") Long id) { Entitlement entitlement = Entitlement .find("select e from Entitlement e left join fetch e.supportLevels where e.id = ?1", id).firstResult(); if (entitlement == null) { @@ -127,14 +125,6 @@ private String hourLabel(Integer code) { return ""; } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - public record EntitlementListResponse(String createPath, List items) { } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementResource.java index 39002b76..d5d8057e 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/EntitlementResource.java @@ -13,11 +13,14 @@ import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.model.Version; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -41,27 +44,28 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed("admin") public class EntitlementResource { + @Inject + CurrentUser currentUser; + @jakarta.inject.Inject ai.mnemosyne_systems.service.EventService eventService; @GET - public Response listEntitlements(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response listEntitlements() { return Response.seeOther(URI.create("/entitlements")).build(); } @GET @Path("create") - public Response createForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response createForm() { return Response.seeOther(URI.create("/entitlements/new")).build(); } @GET @Path("{id}") - public Response viewEntitlement(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response viewEntitlement(@PathParam("id") Long id) { if (Entitlement.findById(id) == null) { throw new NotFoundException(); } @@ -70,8 +74,7 @@ public Response viewEntitlement(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("{id}/edit") - public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response editForm(@PathParam("id") Long id) { if (Entitlement.findById(id) == null) { throw new NotFoundException(); } @@ -81,12 +84,10 @@ public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path @POST @Path("") @Transactional - public Response createEntitlement(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, + public Response createEntitlement(@HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, @FormParam("description") String description, @FormParam("levelIds") List levelIds, @FormParam("versionIds") List versionIds, @FormParam("versionNames") List versionNames, @FormParam("versionDates") List versionDates) { - requireAdmin(auth); validate(name, description); Entitlement entitlement = new Entitlement(); entitlement.name = name.trim(); @@ -95,20 +96,20 @@ public Response createEntitlement(@CookieParam(AuthHelper.AUTH_COOKIE) String au entitlement.versions.clear(); entitlement.versions.addAll(resolveVersions(entitlement, null, versionIds, versionNames, versionDates)); entitlement.persist(); + User user = currentUser.get(); eventService.record(entitlement.id, ai.mnemosyne_systems.model.event.EventConstants.ENTITLEMENT_CREATED, null, - AuthHelper.findUser(auth).id, "Entitlement created"); + user.id, "Entitlement created"); return ReactRedirectSupport.redirect(client, "/entitlements"); } @POST @Path("{id}") @Transactional - public Response updateEntitlement(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, - @FormParam("description") String description, @FormParam("levelIds") List levelIds, - @FormParam("versionIds") List versionIds, @FormParam("versionNames") List versionNames, + public Response updateEntitlement(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + @FormParam("name") String name, @FormParam("description") String description, + @FormParam("levelIds") List levelIds, @FormParam("versionIds") List versionIds, + @FormParam("versionNames") List versionNames, @FormParam("versionDates") List versionDates) { - requireAdmin(auth); Entitlement entitlement = Entitlement .find("select e from Entitlement e left join fetch e.supportLevels where e.id = ?1", id).firstResult(); if (entitlement == null) { @@ -129,15 +130,14 @@ public Response updateEntitlement(@CookieParam(AuthHelper.AUTH_COOKIE) String au @POST @Path("{id}/delete") @Transactional - public Response deleteEntitlement(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { - requireAdmin(auth); + public Response deleteEntitlement(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { Entitlement entitlement = Entitlement.findById(id); if (entitlement == null) { throw new NotFoundException(); } + User user = currentUser.get(); eventService.record(entitlement.id, ai.mnemosyne_systems.model.event.EventConstants.ENTITLEMENT_DELETED, null, - AuthHelper.findUser(auth).id, "Entitlement deleted"); + user.id, "Entitlement deleted"); entitlement.delete(); return ReactRedirectSupport.redirect(client, "/entitlements"); } @@ -218,12 +218,4 @@ private Long parseVersionId(String value) { throw new BadRequestException("Version id is invalid"); } } - - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/HomeResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/HomeResource.java index 132d7af1..00a88a71 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/HomeResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/HomeResource.java @@ -10,8 +10,11 @@ import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; -import jakarta.ws.rs.CookieParam; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; + import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -22,11 +25,15 @@ @Path("/") @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class HomeResource { + @Inject + CurrentUser currentUser; + @GET - public Object index(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryParam("error") String error) { - User user = AuthHelper.findUser(auth); + public Object index(@QueryParam("error") String error) { + User user = currentUser.get(); if (AuthHelper.isAdmin(user) || AuthHelper.isSupport(user) || AuthHelper.isSuperuser(user) || AuthHelper.isUser(user)) { return Response.status(Response.Status.SEE_OTHER).header("Location", "/").build(); @@ -37,8 +44,8 @@ public Object index(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryPara @GET @Path("/admin") - public Object adminHome(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - User user = AuthHelper.findUser(auth); + public Object adminHome() { + User user = currentUser.get(); if (!AuthHelper.isAdmin(user)) { return Response.status(Response.Status.SEE_OTHER).header("Location", "/").build(); } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/LevelApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/LevelApiResource.java index e47f8d8e..5770bb11 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/LevelApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/LevelApiResource.java @@ -13,8 +13,11 @@ import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.Path; @@ -26,12 +29,15 @@ @Path("/api/levels") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("admin") public class LevelApiResource { + @Inject + CurrentUser currentUser; + @GET @Transactional - public LevelListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public LevelListResponse list() { List levels = Level.list("order by name"); return new LevelListResponse("/levels/new", levels.stream().map(this::toListItem).toList()); } @@ -39,16 +45,14 @@ public LevelListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) @GET @Path("/bootstrap") @Transactional - public LevelBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public LevelBootstrapResponse bootstrap() { return bootstrapResponse(); } @GET @Path("/{id}") @Transactional - public LevelDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public LevelDetailResponse detail(@PathParam("id") Long id) { Level level = Level.findById(id); if (level == null) { throw new jakarta.ws.rs.NotFoundException(); @@ -143,14 +147,6 @@ private String formatColorDisplay(String color) { return "(" + color + ")"; } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - public record LevelListResponse(String createPath, List items) { } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/LevelResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/LevelResource.java index 4d71bee4..433da67f 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/LevelResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/LevelResource.java @@ -12,12 +12,14 @@ import ai.mnemosyne_systems.model.Level; import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; -import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -36,7 +38,11 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed("admin") public class LevelResource { + @Inject + CurrentUser currentUser; + @jakarta.inject.Inject ai.mnemosyne_systems.service.EventService eventService; @@ -73,22 +79,19 @@ public String getDisplay() { new ColorOption("Teal"), new ColorOption("Aqua")); @GET - public Response listLevels(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response listLevels() { return Response.seeOther(URI.create("/levels")).build(); } @GET @Path("create") - public Response createForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response createForm() { return Response.seeOther(URI.create("/levels/new")).build(); } @GET @Path("{id}/edit") - public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response editForm(@PathParam("id") Long id) { if (Level.findById(id) == null) { throw new NotFoundException(); } @@ -98,14 +101,12 @@ public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path @POST @Path("") @Transactional - public Response createLevel(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, + public Response createLevel(@HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, @FormParam("description") String description, @FormParam("level") Integer levelValue, @FormParam("color") String color, @FormParam("fromDay") Integer fromDay, @FormParam("fromTime") Integer fromTime, @FormParam("toDay") Integer toDay, @FormParam("toTime") Integer toTime, @FormParam("countryId") Long countryId, @FormParam("timezoneId") Long timezoneId) { - requireAdmin(auth); Integer normalizedFromDay = normalizeDay(fromDay, Level.DayOption.MONDAY.getCode()); Integer normalizedFromTime = normalizeTime(fromTime, Level.HourOption.H00.getCode()); Integer normalizedToDay = normalizeDay(toDay, Level.DayOption.SUNDAY.getCode()); @@ -126,22 +127,21 @@ public Response createLevel(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, level.timezone = timezoneId != null ? Timezone.findById(timezoneId) : Timezone.find("name", "America/New_York").firstResult(); level.persist(); - eventService.record(level.id, ai.mnemosyne_systems.model.event.EventConstants.LEVEL_CREATED, null, - AuthHelper.findUser(auth).id, "Level created"); + User user = currentUser.get(); + eventService.record(level.id, ai.mnemosyne_systems.model.event.EventConstants.LEVEL_CREATED, null, user.id, + "Level created"); return ReactRedirectSupport.redirect(client, "/levels"); } @POST @Path("{id}") @Transactional - public Response updateLevel(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, - @FormParam("description") String description, @FormParam("level") Integer levelValue, - @FormParam("color") String color, @FormParam("fromDay") Integer fromDay, - @FormParam("fromTime") Integer fromTime, @FormParam("toDay") Integer toDay, - @FormParam("toTime") Integer toTime, @FormParam("countryId") Long countryId, - @FormParam("timezoneId") Long timezoneId) { - requireAdmin(auth); + public Response updateLevel(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + @FormParam("name") String name, @FormParam("description") String description, + @FormParam("level") Integer levelValue, @FormParam("color") String color, + @FormParam("fromDay") Integer fromDay, @FormParam("fromTime") Integer fromTime, + @FormParam("toDay") Integer toDay, @FormParam("toTime") Integer toTime, + @FormParam("countryId") Long countryId, @FormParam("timezoneId") Long timezoneId) { Level level = Level.findById(id); if (level == null) { throw new NotFoundException(); @@ -174,15 +174,14 @@ public Response updateLevel(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @P @POST @Path("{id}/delete") @Transactional - public Response deleteLevel(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { - requireAdmin(auth); + public Response deleteLevel(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { Level level = Level.findById(id); if (level == null) { throw new NotFoundException(); } - eventService.record(level.id, ai.mnemosyne_systems.model.event.EventConstants.LEVEL_DELETED, null, - AuthHelper.findUser(auth).id, "Level deleted"); + User user = currentUser.get(); + eventService.record(level.id, ai.mnemosyne_systems.model.event.EventConstants.LEVEL_DELETED, null, user.id, + "Level deleted"); level.delete(); return ReactRedirectSupport.redirect(client, "/levels"); } @@ -254,12 +253,4 @@ private ColorOption resolveColorOption(String value) { } return null; } - - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/LogoutResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/LogoutResource.java index 42e9844d..724c7b4a 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/LogoutResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/LogoutResource.java @@ -6,10 +6,12 @@ * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. */ +// We don't need it anymore. package ai.mnemosyne_systems.resource; import ai.mnemosyne_systems.util.AuthHelper; import io.smallrye.common.annotation.Blocking; + import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerApiResource.java index f064a91c..9fa7575f 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerApiResource.java @@ -16,8 +16,9 @@ import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -33,19 +34,18 @@ @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Blocking +@RolesAllowed("admin") public class OwnerApiResource { @GET @Transactional - public OwnerResponse owner(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public OwnerResponse owner() { return toResponse(OwnerResource.findOwnerCompany()); } @POST @Transactional - public OwnerResponse update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, OwnerUpdateRequest request) { - requireAdmin(auth); + public OwnerResponse update(OwnerUpdateRequest request) { if (request == null || request.name() == null || request.name().isBlank()) { throw new WebApplicationException("Name is required", Response.Status.BAD_REQUEST); } @@ -89,14 +89,6 @@ public OwnerResponse update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, Ow return toResponse(company); } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - private OwnerResponse toResponse(Company company) { Installation installation = OwnerResource.findOrCreateInstallation(company); List supportUsers = company.users.stream() diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerResource.java index 665c58a8..ed75f002 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/OwnerResource.java @@ -16,9 +16,10 @@ import ai.mnemosyne_systems.infra.BrandingProvider; import ai.mnemosyne_systems.util.AuthHelper; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; @@ -36,34 +37,31 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed("admin") public class OwnerResource { @GET - public Response viewOwner(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response viewOwner() { return Response.seeOther(URI.create("/owner")).build(); } @GET @Path("/edit") - public Response editOwner(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response editOwner() { return Response.seeOther(URI.create("/owner/edit")).build(); } @POST @Path("/edit") @Transactional - public Response updateOwner(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @FormParam("name") String name, - @FormParam("address1") String address1, @FormParam("address2") String address2, - @FormParam("city") String city, @FormParam("state") String state, @FormParam("zip") String zip, - @FormParam("countryId") Long countryId, @FormParam("timezoneId") Long timezoneId, - @FormParam("phoneNumber") String phoneNumber, @FormParam("supportIds") List supportIds, - @FormParam("tamIds") List tamIds, @FormParam("headerFooterColor") String headerFooterColor, - @FormParam("headersColor") String headersColor, @FormParam("buttonsColor") String buttonsColor, - @FormParam("use24HourClock") Boolean use24HourClock, + public Response updateOwner(@FormParam("name") String name, @FormParam("address1") String address1, + @FormParam("address2") String address2, @FormParam("city") String city, @FormParam("state") String state, + @FormParam("zip") String zip, @FormParam("countryId") Long countryId, + @FormParam("timezoneId") Long timezoneId, @FormParam("phoneNumber") String phoneNumber, + @FormParam("supportIds") List supportIds, @FormParam("tamIds") List tamIds, + @FormParam("headerFooterColor") String headerFooterColor, @FormParam("headersColor") String headersColor, + @FormParam("buttonsColor") String buttonsColor, @FormParam("use24HourClock") Boolean use24HourClock, @FormParam("ticketAutoCloseDays") Integer ticketAutoCloseDays) { - requireAdmin(auth); Company company = findOwnerCompany(); if (name == null || name.isBlank()) { throw new jakarta.ws.rs.BadRequestException("Name is required"); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileApiResource.java index 1b232152..20a33d7c 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileApiResource.java @@ -14,10 +14,13 @@ import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.quarkus.elytron.security.common.BcryptUtil; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.POST; @@ -30,20 +33,24 @@ @Path("/api/profile") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class ProfileApiResource { + @Inject + CurrentUser currentUser; + @GET @Transactional - public ProfileResponse profile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - User user = requireUser(auth); + public ProfileResponse profile() { + User user = currentUser.get(); return toResponse(user); } @POST @Consumes(MediaType.APPLICATION_JSON) @Transactional - public ProfileResponse update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, ProfileUpdateRequest request) { - User user = requireUser(auth); + public ProfileResponse update(ProfileUpdateRequest request) { + User user = currentUser.get(); if (request == null || request.name() == null || request.name().isBlank()) { throw new WebApplicationException("Username is required", Response.Status.BAD_REQUEST); } @@ -73,9 +80,8 @@ public ProfileResponse update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path("/password") @Consumes(MediaType.APPLICATION_JSON) @Transactional - public PasswordResponse updatePassword(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - PasswordUpdateRequest request) { - User user = requireUser(auth); + public PasswordResponse updatePassword(PasswordUpdateRequest request) { + User user = currentUser.get(); if (request == null || request.oldPassword() == null || request.oldPassword().isBlank()) { throw new WebApplicationException("Old password is required", Response.Status.BAD_REQUEST); } @@ -179,14 +185,6 @@ private Integer normalizePageSize(Integer value) { return Math.min(value, PaginationSupport.MAX_PAGE_SIZE); } - private User requireUser(String auth) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - public record ProfileResponse(String role, String username, String displayName, String email, String fullName, String social, String phoneNumber, String phoneExtension, Long countryId, String countryName, Long timezoneId, String timezoneName, String logoBase64, String emailFormat, Integer pageSize, diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileResource.java index fccfd028..0c149afb 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/ProfileResource.java @@ -14,12 +14,14 @@ import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.quarkus.elytron.security.common.BcryptUtil; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -39,21 +41,24 @@ @Path("/profile") @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class ProfileResource { @Inject Logger logger; + @Inject + CurrentUser currentUser; + @GET - public Object edit(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireUser(auth); + public Object edit() { return Response.seeOther(URI.create("/profile")).build(); } @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Transactional - public Object update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, MultivaluedMap form) { - User user = requireUser(auth); + public Object update(MultivaluedMap form) { + User user = currentUser.get(); String name = value(form, "name"); if (name == null || name.isBlank()) { return profileErrorRedirect("Username is required"); @@ -175,8 +180,7 @@ private String normalizeEmailFormat(String value) { @GET @Path("/password") - public Object passwordForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireUser(auth); + public Object passwordForm() { return Response.seeOther(URI.create("/profile/password")).build(); } @@ -184,10 +188,9 @@ public Object passwordForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { @Path("/password") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Transactional - public Object updatePassword(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @FormParam("oldPassword") String oldPassword, @FormParam("newPassword") String newPassword, - @FormParam("confirmPassword") String confirmPassword) { - User user = requireUser(auth); + public Object updatePassword(@FormParam("oldPassword") String oldPassword, + @FormParam("newPassword") String newPassword, @FormParam("confirmPassword") String confirmPassword) { + User user = currentUser.get(); if (oldPassword == null || oldPassword.isBlank()) { return passwordErrorRedirect("Old password is required"); } @@ -212,14 +215,6 @@ private Response passwordErrorRedirect(String error) { return Response.seeOther(URI.create("/profile/password?error=" + encodeQueryValue(error))).build(); } - private User requireUser(String auth) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - private String value(MultivaluedMap form, String key) { if (form == null) { return null; diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/ReportApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/ReportApiResource.java index 46d593c9..c9a9be8d 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/ReportApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/ReportApiResource.java @@ -8,24 +8,24 @@ package ai.mnemosyne_systems.resource; -import ai.mnemosyne_systems.model.Category; import ai.mnemosyne_systems.model.Company; import ai.mnemosyne_systems.model.Message; import ai.mnemosyne_systems.model.ReportData; import ai.mnemosyne_systems.model.Ticket; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import java.net.URI; import java.time.Duration; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -36,6 +36,7 @@ @Path("/api/reports") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed({ "admin", "tam", "superuser" }) public class ReportApiResource { private static final String BUCKET_UNDER_1H = "< 1h"; private static final String BUCKET_1_TO_8H = "1–8h"; @@ -43,11 +44,13 @@ public class ReportApiResource { private static final String BUCKET_1_TO_7D = "1–7 days"; private static final String BUCKET_OVER_7D = "> 7 days"; + @Inject + CurrentUser currentUser; + @GET @Transactional - public ReportResponse reports(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("period") String period) { - User user = requireReporter(auth); + public ReportResponse reports(@QueryParam("companyId") Long companyId, @QueryParam("period") String period) { + User user = currentUser.get(); String safePeriod = period == null || period.isBlank() ? "all" : period.toLowerCase(); if (AuthHelper.isAdmin(user)) { List companies = Company.list( @@ -112,18 +115,6 @@ private List toHistogram(Map> histogram) { .toList(); } - private User requireReporter(String auth) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - if (AuthHelper.isAdmin(user) || User.TYPE_TAM.equalsIgnoreCase(user.type) - || User.TYPE_SUPERUSER.equalsIgnoreCase(user.type)) { - return user; - } - throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN).location(URI.create("/")).build()); - } - private ReportData buildReportData(List filterCompanies, String period) { List tickets; if (filterCompanies != null && !filterCompanies.isEmpty()) { diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/ReportResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/ReportResource.java index 560c02f7..d48d8cc6 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/ReportResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/ReportResource.java @@ -11,7 +11,9 @@ import ai.mnemosyne_systems.model.*; import ai.mnemosyne_systems.service.PdfService; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; @@ -28,6 +30,7 @@ @Path("/reports") @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed({ "admin", "tam", "superuser" }) public class ReportResource { private static final String BUCKET_UNDER_1H = "< 1h"; private static final String BUCKET_1_TO_8H = "1–8h"; @@ -38,26 +41,26 @@ public class ReportResource { @Inject PdfService pdfService; + @Inject + CurrentUser currentUser; + @GET - public Object adminReports(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("period") String period) { - requireAdmin(auth); + @RolesAllowed("admin") + public Object adminReports(@QueryParam("companyId") Long companyId, @QueryParam("period") String period) { return Response.seeOther(URI.create("/reports" + reportQuery(companyId, period))).build(); } @GET @Path("/tam") - public Object tamReports(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryParam("companyId") Long companyId, - @QueryParam("period") String period) { - requireTam(auth); + @RolesAllowed("tam") + public Object tamReports(@QueryParam("companyId") Long companyId, @QueryParam("period") String period) { return Response.seeOther(URI.create("/reports" + reportQuery(companyId, period))).build(); } @GET @Path("/superuser") - public Object superuserReports(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("period") String period) { - requireSuperuser(auth); + @RolesAllowed("superuser") + public Object superuserReports(@QueryParam("companyId") Long companyId, @QueryParam("period") String period) { return Response.seeOther(URI.create("/reports" + reportQuery(companyId, period))).build(); } @@ -76,14 +79,13 @@ private String reportQuery(Long companyId, String period) { @Path("/export") @Produces("application/pdf") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response exportAdminReport(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("period") String period, + @RolesAllowed("admin") + public Response exportAdminReport(@QueryParam("companyId") Long companyId, @QueryParam("period") String period, @FormParam("statusChart") String statusChart, @FormParam("categoryChart") String categoryChart, @FormParam("companyChart") String companyChart, @FormParam("timeChart") String timeChart, @FormParam("responseTimeChart") String responseTimeChart, @FormParam("resolutionTimeChart") String resolutionTimeChart, @FormParam("histogramChart") String histogramChart) { - requireAdmin(auth); Company selectedCompany = companyId != null ? Company.findById(companyId) : null; String safePeriod = period == null || period.isBlank() ? "all" : period.toLowerCase(); ReportData data = buildReportData(selectedCompany != null ? List.of(selectedCompany) : null, safePeriod); @@ -99,14 +101,14 @@ public Response exportAdminReport(@CookieParam(AuthHelper.AUTH_COOKIE) String au @Path("/tam/export") @Produces("application/pdf") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response exportTamReport(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("period") String period, + @RolesAllowed("tam") + public Response exportTamReport(@QueryParam("companyId") Long companyId, @QueryParam("period") String period, @FormParam("statusChart") String statusChart, @FormParam("categoryChart") String categoryChart, @FormParam("companyChart") String companyChart, @FormParam("timeChart") String timeChart, @FormParam("responseTimeChart") String responseTimeChart, @FormParam("resolutionTimeChart") String resolutionTimeChart, @FormParam("histogramChart") String histogramChart) { - User user = requireTam(auth); + User user = currentUser.get(); List tamCompanies = Company.list( "select distinct c from Company c join c.users u where u = ?1 and exists (select t from Ticket t where t.company = c) order by c.name", user); @@ -129,14 +131,14 @@ public Response exportTamReport(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @Path("/superuser/export") @Produces("application/pdf") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response exportSuperuserReport(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("period") String period, + @RolesAllowed("superuser") + public Response exportSuperuserReport(@QueryParam("companyId") Long companyId, @QueryParam("period") String period, @FormParam("statusChart") String statusChart, @FormParam("categoryChart") String categoryChart, @FormParam("companyChart") String companyChart, @FormParam("timeChart") String timeChart, @FormParam("responseTimeChart") String responseTimeChart, @FormParam("resolutionTimeChart") String resolutionTimeChart, @FormParam("histogramChart") String histogramChart) { - User user = requireSuperuser(auth); + User user = currentUser.get(); List superuserCompanies = Company.list( "select distinct c from Company c join c.users u where u = ?1 and exists (select t from Ticket t where t.company = c) order by c.name", user); @@ -382,27 +384,4 @@ private Map buildChartImages(String statusChart, String category return images; } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isAdmin(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - - private User requireTam(String auth) { - User user = AuthHelper.findUser(auth); - if (user == null || !User.TYPE_TAM.equalsIgnoreCase(user.type)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - - private User requireSuperuser(String auth) { - User user = AuthHelper.findUser(auth); - if (user == null || !User.TYPE_SUPERUSER.equalsIgnoreCase(user.type)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/RssResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/RssResource.java index 5785f08a..e26c004e 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/RssResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/RssResource.java @@ -12,8 +12,11 @@ import ai.mnemosyne_systems.model.Ticket; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; -import jakarta.ws.rs.CookieParam; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; + import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -37,19 +40,17 @@ @Blocking public class RssResource { + @Inject + CurrentUser currentUser; + private static final DateTimeFormatter RFC_1123 = DateTimeFormatter.RFC_1123_DATE_TIME; @GET @Path("/support") @Produces("application/rss+xml") - public Response support(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Context UriInfo uriInfo) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - if (!AuthHelper.isSupport(user)) { - throw new ForbiddenException(); - } + @RolesAllowed("support") + public Response support(@Context UriInfo uriInfo) { + User user = currentUser.get(); List assigned = Ticket.find( "select distinct t from Ticket t join t.supportUsers u where u = ?1 and (t.status is null or lower(t.status) <> 'closed')", user).list(); @@ -65,14 +66,9 @@ public Response support(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Conte @GET @Path("/tam") @Produces("application/rss+xml") - public Response tam(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Context UriInfo uriInfo) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - if (!User.TYPE_TAM.equalsIgnoreCase(user.type)) { - throw new ForbiddenException(); - } + @RolesAllowed("tam") + public Response tam(@Context UriInfo uriInfo) { + User user = currentUser.get(); List scoped = Ticket.list( "select distinct t from Ticket t left join t.tamUsers tu left join t.company c left join c.users cu where tu = ?1 or cu = ?1", user); @@ -95,14 +91,9 @@ public Response tam(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Context U @GET @Path("/superuser") @Produces("application/rss+xml") - public Response superuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Context UriInfo uriInfo) { - User user = AuthHelper.findUser(auth); - if (user == null) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - if (!User.TYPE_SUPERUSER.equalsIgnoreCase(user.type)) { - throw new ForbiddenException(); - } + @RolesAllowed("superuser") + public Response superuser(@Context UriInfo uriInfo) { + User user = currentUser.get(); List scoped = SuperuserResource.loadScopedTickets(user); LinkedHashMap merged = new LinkedHashMap<>(); for (Ticket ticket : scoped) { diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserDirectoryApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserDirectoryApiResource.java index 62b3615a..9717e6a3 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserDirectoryApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserDirectoryApiResource.java @@ -14,8 +14,11 @@ import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotFoundException; @@ -29,17 +32,19 @@ @Path("/api/superuser") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("superuser") public class SuperuserDirectoryApiResource { + @Inject + CurrentUser currentUser; + @GET @Path("/users") @Transactional - public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId) { - User currentUser = requireSuperuser(auth); + public UserDirectoryApiModels.DirectoryListResponse list(@QueryParam("companyId") Long companyId) { + User u = currentUser.get(); List companies = Company - .find("select distinct c from Company c join c.users u where u = ?1 order by c.name", currentUser) - .list(); + .find("select distinct c from Company c join c.users u where u = ?1 order by c.name", u).list(); Company selectedCompany = selectCompany(companies, companyId); List users = selectedCompany == null ? List.of() : Company. find("select u from Company c join c.users u where c = ?1 order by u.name", @@ -55,12 +60,11 @@ public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam(AuthHelper @GET @Path("/users/bootstrap") @Transactional - public UserDirectoryApiModels.UserFormResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("countryId") Long countryId) { - User currentUser = requireSuperuser(auth); + public UserDirectoryApiModels.UserFormResponse bootstrap(@QueryParam("companyId") Long companyId, + @QueryParam("countryId") Long countryId) { + User u = currentUser.get(); List companies = Company - .find("select distinct c from Company c join c.users u where u = ?1 order by c.name", currentUser) - .list(); + .find("select distinct c from Company c join c.users u where u = ?1 order by c.name", u).list(); Company selectedCompany = selectCompany(companies, companyId); if (selectedCompany == null) { throw new NotFoundException(); @@ -87,43 +91,39 @@ public UserDirectoryApiModels.UserFormResponse bootstrap(@CookieParam(AuthHelper @GET @Path("/support-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse supportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User currentUser = requireSuperuser(auth); + public UserDirectoryApiModels.UserDetailResponse supportUser(@PathParam("id") Long id) { + User u = currentUser.get(); User user = userByType(id, User.TYPE_SUPPORT); - return detailResponse(currentUser, user, canViewSupportUser(currentUser, user)); + return detailResponse(u, user, canViewSupportUser(u, user)); } @GET @Path("/superuser-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse superuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User currentUser = requireSuperuser(auth); + public UserDirectoryApiModels.UserDetailResponse superuser(@PathParam("id") Long id) { + User u = currentUser.get(); User user = userByType(id, User.TYPE_SUPERUSER); - return detailResponse(currentUser, user, false); + return detailResponse(u, user, false); } @GET @Path("/user-profiles/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse profile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User currentUser = requireSuperuser(auth); + public UserDirectoryApiModels.UserDetailResponse profile(@PathParam("id") Long id) { + User u = currentUser.get(); User user = User.findById(id); if (user == null) { throw new NotFoundException(); } - return detailResponse(currentUser, user, false); + return detailResponse(u, user, false); } @GET @Path("/companies/{id}") @Transactional - public UserDirectoryApiModels.CompanyDetailResponse company(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User currentUser = requireSuperuser(auth); - Company company = allowedCompany(currentUser, id); + public UserDirectoryApiModels.CompanyDetailResponse company(@PathParam("id") Long id) { + User u = currentUser.get(); + Company company = allowedCompany(u, id); return new UserDirectoryApiModels.CompanyDetailResponse(company.id, company.name, company.address1, company.address2, company.city, company.state, company.zip, company.phoneNumber, company.country == null ? null : company.country.name, diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserResource.java index 6bb7c9ce..7de6bb9e 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserResource.java @@ -24,13 +24,15 @@ import ai.mnemosyne_systems.util.AttachmentHelper; import ai.mnemosyne_systems.util.AuthHelper; import ai.mnemosyne_systems.util.TicketTimeSupport; +import ai.mnemosyne_systems.util.CurrentUser; import io.quarkus.elytron.security.common.BcryptUtil; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -58,11 +60,15 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed("superuser") public class SuperuserResource { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d yyyy, h.mma", Locale.ENGLISH); + @Inject + CurrentUser currentUser; + @Inject CrossReferenceService crossReferenceService; @@ -74,32 +80,28 @@ public class SuperuserResource { @GET @Path("superuser") - public Response home(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSuperuser(auth); + public Response home() { return Response.seeOther(URI.create("/superuser/tickets")).build(); } @GET @Path("superuser/users") - public Response usersRoot(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSuperuser(auth); + public Response usersRoot() { return Response.seeOther(URI.create("/superuser/users")).build(); } @GET @Path("superuser/users/{companyId}") - public Response listUsers(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("companyId") Long companyId) { - User user = requireSuperuser(auth); + public Response listUsers(@PathParam("companyId") Long companyId) { + User user = currentUser.get(); Company company = allowedCompany(user, companyId); return Response.seeOther(URI.create("/superuser/users?companyId=" + company.id)).build(); } @GET @Path("superuser/users/{companyId}/create") - public Response createUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("companyId") Long companyId) { - User user = requireSuperuser(auth); + public Response createUserForm(@PathParam("companyId") Long companyId) { + User user = currentUser.get(); Company company = allowedCompany(user, companyId); return Response.seeOther(URI.create("/superuser/users/new?companyId=" + company.id)).build(); } @@ -107,13 +109,13 @@ public Response createUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @POST @Path("superuser/users") @Transactional - public Response createUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @FormParam("name") String name, - @FormParam("fullName") String fullName, @FormParam("email") String email, - @FormParam("social") String social, @FormParam("phoneNumber") String phoneNumber, - @FormParam("phoneExtension") String phoneExtension, @FormParam("timezoneId") Long timezoneId, - @FormParam("countryId") Long countryId, @FormParam("password") String password, - @FormParam("type") String type, @FormParam("companyId") Long companyId) { - User user = requireSuperuser(auth); + public Response createUser(@FormParam("name") String name, @FormParam("fullName") String fullName, + @FormParam("email") String email, @FormParam("social") String social, + @FormParam("phoneNumber") String phoneNumber, @FormParam("phoneExtension") String phoneExtension, + @FormParam("timezoneId") Long timezoneId, @FormParam("countryId") Long countryId, + @FormParam("password") String password, @FormParam("type") String type, + @FormParam("companyId") Long companyId) { + User user = currentUser.get(); if (name == null || name.isBlank()) { throw new BadRequestException("Username is required"); } @@ -151,29 +153,25 @@ public Response createUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Fo @GET @Path("superuser/tickets") - public Response tickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSuperuser(auth); + public Response tickets() { return Response.seeOther(URI.create("/superuser/tickets")).build(); } @GET @Path("superuser/tickets/open") - public Response openTickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSuperuser(auth); + public Response openTickets() { return Response.seeOther(URI.create("/superuser/tickets/open")).build(); } @GET @Path("superuser/tickets/closed") - public Response closedTickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSuperuser(auth); + public Response closedTickets() { return Response.seeOther(URI.create("/superuser/tickets/closed")).build(); } @GET @Path("superuser/tickets/create") - public Response createTicketForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSuperuser(auth); + public Response createTicketForm() { return Response.seeOther(URI.create("/superuser/tickets/new")).build(); } @@ -181,9 +179,8 @@ public Response createTicketForm(@CookieParam(AuthHelper.AUTH_COOKIE) String aut @Path("superuser/tickets") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response createTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { - User user = requireSuperuser(auth); + public Response createTicket(@HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { + User user = currentUser.get(); String status = AttachmentHelper.readFormValue(input, "status"); String title = Ticket.normalizeTitle(AttachmentHelper.readFormValue(input, "title")); String messageBody = AttachmentHelper.readFormValue(input, "message"); @@ -243,8 +240,7 @@ public Response createTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("superuser/support-users/{id}") - public Response viewSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSuperuser(auth); + public Response viewSupportUser(@PathParam("id") Long id) { User supportUser = User.findById(id); if (supportUser == null || !User.TYPE_SUPPORT.equalsIgnoreCase(supportUser.type)) { throw new NotFoundException(); @@ -254,8 +250,8 @@ public Response viewSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("superuser/superuser-users/{id}") - public Response viewSuperuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireSuperuser(auth); + public Response viewSuperuser(@PathParam("id") Long id) { + User user = currentUser.get(); User viewedUser = User.findById(id); if (viewedUser == null || !User.TYPE_SUPERUSER.equalsIgnoreCase(viewedUser.type)) { throw new NotFoundException(); @@ -271,8 +267,8 @@ public Response viewSuperuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("superuser/user-profiles/{id}") - public Response viewUserProfile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireSuperuser(auth); + public Response viewUserProfile(@PathParam("id") Long id) { + User user = currentUser.get(); User viewedUser = User.findById(id); if (viewedUser == null) { throw new NotFoundException(); @@ -288,16 +284,16 @@ public Response viewUserProfile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("superuser/companies/{id}") - public Response viewCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireSuperuser(auth); + public Response viewCompany(@PathParam("id") Long id) { + User user = currentUser.get(); Company company = allowedCompany(user, id); return Response.seeOther(URI.create("/superuser/companies/" + company.id)).build(); } @GET @Path("superuser/tickets/{id}") - public Response ticketDetail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireSuperuser(auth); + public Response ticketDetail(@PathParam("id") Long id) { + User user = currentUser.get(); if (findTicketForSuperuser(user, id) == null) { throw new NotFoundException(); } @@ -308,9 +304,8 @@ public Response ticketDetail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @ @Path("superuser/tickets/{id}/messages") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response addMessage(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - MultipartFormDataInput input) { - User user = requireSuperuser(auth); + public Response addMessage(@PathParam("id") Long id, MultipartFormDataInput input) { + User user = currentUser.get(); String body = AttachmentHelper.readFormValue(input, "body"); if (body == null || body.isBlank()) { throw new BadRequestException("Message is required"); @@ -338,8 +333,8 @@ public Response addMessage(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Pa @GET @Path("superuser/tickets/{id}/edit") - public Response ticketEdit(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireSuperuser(auth); + public Response ticketEdit(@PathParam("id") Long id) { + User user = currentUser.get(); if (findTicketForSuperuser(user, id) == null) { throw new NotFoundException(); } @@ -349,11 +344,10 @@ public Response ticketEdit(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Pa @POST @Path("superuser/tickets/{id}") @Transactional - public Response updateTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id, + public Response updateTicket(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id, @FormParam("title") String title, @FormParam("affectsVersionId") Long affectsVersionId, @FormParam("resolvedVersionId") Long resolvedVersionId) { - User user = requireSuperuser(auth); + User user = currentUser.get(); String normalizedTitle = Ticket.normalizeTitle(title); Ticket ticket = findTicketForSuperuser(user, id); if (ticket == null) { @@ -809,14 +803,6 @@ String formatDate(LocalDateTime date) { return formatted.replace("AM", "am").replace("PM", "pm"); } - private User requireSuperuser(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isSuperuser(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - static class SupportTicketData { List assignedTickets; List openTickets; diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserTicketApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserTicketApiResource.java index c01783b5..1537ea4f 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserTicketApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserTicketApiResource.java @@ -20,9 +20,11 @@ import ai.mnemosyne_systems.service.CrossReferenceService; import ai.mnemosyne_systems.service.EventService; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; @@ -41,8 +43,12 @@ @Path("/api/superuser/tickets") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("superuser") public class SuperuserTicketApiResource { + @Inject + CurrentUser currentUser; + @Inject CrossReferenceService crossReferenceService; @@ -54,11 +60,11 @@ public class SuperuserTicketApiResource { @GET @Transactional - public SupportTicketApiResource.SupportTicketListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, + public SupportTicketApiResource.SupportTicketListResponse list( @QueryParam("view") @DefaultValue("assigned") String view, @QueryParam("q") String q, @QueryParam("page") Integer page, @QueryParam("pageSize") Integer pageSize, @QueryParam("sort") String sort, @QueryParam("dir") String dir) { - User user = requireSuperuser(auth); + User user = currentUser.get(); SuperuserResource.SupportTicketData data = superuserResource.buildTicketDataForUser(user); String normalizedView = normalizeView(view); List tickets = switch (normalizedView) { @@ -91,10 +97,10 @@ public SupportTicketApiResource.SupportTicketListResponse list(@CookieParam(Auth @GET @Path("/suggest") @Transactional - public SupportTicketApiResource.TicketSuggestionResponse suggest(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, + public SupportTicketApiResource.TicketSuggestionResponse suggest( @QueryParam("view") @DefaultValue("assigned") String view, @QueryParam("q") String q, @QueryParam("exclude") Long exclude) { - User user = requireSuperuser(auth); + User user = currentUser.get(); SuperuserResource.SupportTicketData data = superuserResource.buildTicketDataForUser(user); List tickets = TicketSearchSupport.combineTickets(data.assignedTickets, data.openTickets, data.closedTickets); @@ -110,10 +116,9 @@ public SupportTicketApiResource.TicketSuggestionResponse suggest(@CookieParam(Au @GET @Path("/bootstrap") @Transactional - public SupportTicketApiResource.SupportTicketBootstrapResponse bootstrap( - @CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryParam("companyId") Long companyId, + public SupportTicketApiResource.SupportTicketBootstrapResponse bootstrap(@QueryParam("companyId") Long companyId, @QueryParam("companyEntitlementId") Long companyEntitlementId) { - User user = requireSuperuser(auth); + User user = currentUser.get(); SuperuserResource.SupportTicketData data = superuserResource.buildTicketDataForUser(user); List companies = superuserResource.userCompanies(user); Company selectedCompany = selectCompany(companies, companyId); @@ -150,9 +155,8 @@ Category. list("order by name").stream().map(this::toCategoryOption).t @GET @Path("/{id}") @Transactional - public UserTicketApiResource.RoleTicketDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User user = requireSuperuser(auth); + public UserTicketApiResource.RoleTicketDetailResponse detail(@PathParam("id") Long id) { + User user = currentUser.get(); Ticket ticket = superuserResource.findTicketForSuperuser(user, id); if (ticket == null) { throw new NotFoundException(); @@ -212,9 +216,7 @@ Category. list("order by name").stream().map(this::toCategoryOption).t @GET @Path("/{id}/references") @Transactional - public CrossReferenceService.CrossReferencesResponse references(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireSuperuser(auth); + public CrossReferenceService.CrossReferencesResponse references(@PathParam("id") Long id) { Ticket ticket = Ticket.findById(id); if (ticket == null) { throw new NotFoundException(); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/SupportResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/SupportResource.java index 71be4299..fb716431 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/SupportResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/SupportResource.java @@ -23,15 +23,16 @@ import ai.mnemosyne_systems.service.EventService; import ai.mnemosyne_systems.service.TicketEmailService; import ai.mnemosyne_systems.util.AttachmentHelper; -import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.quarkus.elytron.security.common.BcryptUtil; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -56,7 +57,12 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed("support") public class SupportResource { + + @Inject + CurrentUser currentUser; + @Inject CrossReferenceService crossReferenceService; @@ -66,29 +72,25 @@ public class SupportResource { EventService eventService; @GET - public Response listTickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSupport(auth); + public Response listTickets() { return Response.seeOther(URI.create("/support/tickets")).build(); } @GET @Path("/open") - public Response listOpenTickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSupport(auth); + public Response listOpenTickets() { return Response.seeOther(URI.create("/support/tickets/open")).build(); } @GET @Path("/closed") - public Response listClosedTickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSupport(auth); + public Response listClosedTickets() { return Response.seeOther(URI.create("/support/tickets/closed")).build(); } @GET @Path("/support-users/{id}") - public Response viewSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSupport(auth); + public Response viewSupportUser(@PathParam("id") Long id) { User supportUser = User.findById(id); if (supportUser == null || !User.TYPE_SUPPORT.equalsIgnoreCase(supportUser.type)) { throw new NotFoundException(); @@ -98,8 +100,7 @@ public Response viewSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("/tam-users/{id}") - public Response viewTamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSupport(auth); + public Response viewTamUser(@PathParam("id") Long id) { User tamUser = User.findById(id); if (tamUser == null || !User.TYPE_TAM.equalsIgnoreCase(tamUser.type)) { throw new NotFoundException(); @@ -109,8 +110,7 @@ public Response viewTamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @P @GET @Path("/superuser-users/{id}") - public Response viewSuperuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSupport(auth); + public Response viewSuperuser(@PathParam("id") Long id) { User superuser = User.findById(id); if (superuser == null || !User.TYPE_SUPERUSER.equalsIgnoreCase(superuser.type)) { throw new NotFoundException(); @@ -120,8 +120,7 @@ public Response viewSuperuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("/user-profiles/{id}") - public Response viewUserProfile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSupport(auth); + public Response viewUserProfile(@PathParam("id") Long id) { User viewedUser = User.findById(id); if (viewedUser == null) { throw new NotFoundException(); @@ -131,8 +130,7 @@ public Response viewUserProfile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("/companies/{id}") - public Response viewCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSupport(auth); + public Response viewCompany(@PathParam("id") Long id) { Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); @@ -142,16 +140,13 @@ public Response viewCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @P @GET @Path("/users") - public Response supportUsersRoot(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSupport(auth); + public Response supportUsersRoot() { return Response.seeOther(URI.create("/support/users")).build(); } @GET @Path("/users/{companyId}") - public Response listSupportUsers(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("companyId") Long companyId) { - requireSupport(auth); + public Response listSupportUsers(@PathParam("companyId") Long companyId) { List companies = Company.list("order by name"); Company selectedCompany = null; if (companyId != null) { @@ -168,9 +163,7 @@ public Response listSupportUsers(@CookieParam(AuthHelper.AUTH_COOKIE) String aut @GET @Path("/users/{companyId}/create") - public Response createSupportUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("companyId") Long companyId) { - requireSupport(auth); + public Response createSupportUserForm(@PathParam("companyId") Long companyId) { Company selectedCompany = Company.findById(companyId); if (selectedCompany == null) { throw new NotFoundException(); @@ -181,14 +174,12 @@ public Response createSupportUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) Strin @POST @Path("/users") @Transactional - public Response createSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @FormParam("name") String name, - @FormParam("fullName") String fullName, @FormParam("email") String email, - @FormParam("social") String social, @FormParam("phoneNumber") String phoneNumber, - @FormParam("phoneExtension") String phoneExtension, @FormParam("timezoneId") Long timezoneId, - @FormParam("countryId") Long countryId, @FormParam("password") String password, - @FormParam("type") String type, @FormParam("companyId") Long companyId, - @Context HttpServletRequest request) { - requireSupport(auth); + public Response createSupportUser(@FormParam("name") String name, @FormParam("fullName") String fullName, + @FormParam("email") String email, @FormParam("social") String social, + @FormParam("phoneNumber") String phoneNumber, @FormParam("phoneExtension") String phoneExtension, + @FormParam("timezoneId") Long timezoneId, @FormParam("countryId") Long countryId, + @FormParam("password") String password, @FormParam("type") String type, + @FormParam("companyId") Long companyId, @Context HttpServletRequest request) { if (name == null || name.isBlank()) { throw new BadRequestException("Username is required"); } @@ -224,8 +215,9 @@ public Response createSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String au newUser.passwordHash = BcryptUtil.bcryptHash(password); } newUser.persist(); + User user = currentUser.get(); eventService.record(newUser.id, ai.mnemosyne_systems.model.event.EventConstants.USER_CREATED, company.id, - AuthHelper.findUser(auth).id, "User created"); + user.id, "User created"); boolean exists = company.users.stream() .anyMatch(existing -> existing.id != null && existing.id.equals(newUser.id)); if (!exists) { @@ -243,20 +235,17 @@ private String trimOrNull(String value) { @GET @Path("/tickets/create") - public Response createForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSupport(auth); + public Response createForm() { return Response.seeOther(URI.create("/support/tickets/new")).build(); } @GET @Path("/tickets/{id}") - public Response ticketDetail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @jakarta.ws.rs.PathParam("id") Long id) { + public Response ticketDetail(@jakarta.ws.rs.PathParam("id") Long id) { Ticket ticket = Ticket.findById(id); if (ticket == null) { throw new NotFoundException(); } - requireSupport(auth); return Response.seeOther(URI.create("/support/tickets/" + id)).build(); } @@ -268,9 +257,8 @@ private List loadMessages(Ticket ticket, User viewer) { @Path("/tickets/{id}/messages") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response addMessage(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @jakarta.ws.rs.PathParam("id") Long id, - MultipartFormDataInput input) { - User user = requireSupport(auth); + public Response addMessage(@jakarta.ws.rs.PathParam("id") Long id, MultipartFormDataInput input) { + User user = currentUser.get(); String body = AttachmentHelper.readFormValue(input, "body"); if (body == null || body.isBlank()) { throw new BadRequestException("Message is required"); @@ -312,9 +300,8 @@ public Response addMessage(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @ja @Path("/tickets") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response createTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { - User user = requireSupport(auth); + public Response createTicket(@HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { + User user = currentUser.get(); String status = AttachmentHelper.readFormValue(input, "status"); String title = Ticket.normalizeTitle(AttachmentHelper.readFormValue(input, "title")); String messageBody = AttachmentHelper.readFormValue(input, "message"); @@ -380,14 +367,14 @@ public Response createTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @POST @Path("/tickets/{id}") @Transactional - public Response updateTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @jakarta.ws.rs.PathParam("id") Long id, - @FormParam("title") String title, @FormParam("status") String status, - @FormParam("companyId") Long companyId, @FormParam("companyEntitlementId") Long companyEntitlementId, - @FormParam("categoryId") Long categoryId, @FormParam("externalIssueLink") String externalIssueLink, + public Response updateTicket(@HeaderParam("X-Billetsys-Client") String client, + @jakarta.ws.rs.PathParam("id") Long id, @FormParam("title") String title, + @FormParam("status") String status, @FormParam("companyId") Long companyId, + @FormParam("companyEntitlementId") Long companyEntitlementId, @FormParam("categoryId") Long categoryId, + @FormParam("externalIssueLink") String externalIssueLink, @FormParam("affectsVersionId") Long affectsVersionId, @FormParam("resolvedVersionId") Long resolvedVersionId) { - User user = requireSupport(auth); + User user = currentUser.get(); String normalizedTitle = Ticket.normalizeTitle(title); Ticket ticket = Ticket.findById(id); if (ticket == null) { @@ -450,9 +437,9 @@ public Response updateTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @POST @Path("/tickets/{id}/assign") @Transactional - public Response assignTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @jakarta.ws.rs.PathParam("id") Long id) { - User user = requireSupport(auth); + public Response assignTicket(@HeaderParam("X-Billetsys-Client") String client, + @jakarta.ws.rs.PathParam("id") Long id) { + User user = currentUser.get(); Ticket ticket = Ticket.findById(id); if (ticket == null) { throw new NotFoundException(); @@ -501,9 +488,7 @@ private void assignCompanyTams(Ticket ticket) { @GET @Path("/tickets/company/{id}/entitlements") - public Response companyEntitlements(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @jakarta.ws.rs.PathParam("id") Long id, @QueryParam("message") String message) { - requireSupport(auth); + public Response companyEntitlements(@jakarta.ws.rs.PathParam("id") Long id, @QueryParam("message") String message) { Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); @@ -595,11 +580,4 @@ static class SupportTicketCounts { } } - private User requireSupport(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isSupport(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/SupportTicketApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/SupportTicketApiResource.java index c97cbcfc..00156a5f 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/SupportTicketApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/SupportTicketApiResource.java @@ -20,9 +20,11 @@ import ai.mnemosyne_systems.util.AuthHelper; import ai.mnemosyne_systems.model.event.Event; import ai.mnemosyne_systems.service.EventService; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.HEAD; @@ -43,8 +45,12 @@ @Path("/api/support/tickets") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("support") public class SupportTicketApiResource { + @Inject + CurrentUser currentUser; + @Inject CrossReferenceService crossReferenceService; @Inject @@ -52,11 +58,10 @@ public class SupportTicketApiResource { @GET @Transactional - public SupportTicketListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("view") @DefaultValue("assigned") String view, @QueryParam("q") String q, - @QueryParam("page") Integer page, @QueryParam("pageSize") Integer pageSize, @QueryParam("sort") String sort, - @QueryParam("dir") String dir) { - User user = requireSupport(auth); + public SupportTicketListResponse list(@QueryParam("view") @DefaultValue("assigned") String view, + @QueryParam("q") String q, @QueryParam("page") Integer page, @QueryParam("pageSize") Integer pageSize, + @QueryParam("sort") String sort, @QueryParam("dir") String dir) { + User user = currentUser.get(); SupportTicketViewSupport.SupportTicketData data = SupportTicketViewSupport.buildTicketData(user); SupportTicketViewSupport.SupportTicketCounts counts = SupportTicketViewSupport.loadTicketCounts(user); String normalizedView = normalizeView(view); @@ -89,10 +94,9 @@ public SupportTicketListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) Strin @GET @Path("/suggest") @Transactional - public TicketSuggestionResponse suggest(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("view") @DefaultValue("assigned") String view, @QueryParam("q") String q, - @QueryParam("exclude") Long exclude) { - User user = requireSupport(auth); + public TicketSuggestionResponse suggest(@QueryParam("view") @DefaultValue("assigned") String view, + @QueryParam("q") String q, @QueryParam("exclude") Long exclude) { + User user = currentUser.get(); SupportTicketViewSupport.SupportTicketData data = SupportTicketViewSupport.buildTicketData(user); String normalizedView = normalizeView(view); List tickets = TicketSearchSupport.combineTickets(data.assignedTickets(), data.openTickets(), @@ -108,9 +112,9 @@ public TicketSuggestionResponse suggest(@CookieParam(AuthHelper.AUTH_COOKIE) Str @GET @Path("/bootstrap") @Transactional - public SupportTicketBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("companyEntitlementId") Long companyEntitlementId) { - User user = requireSupport(auth); + public SupportTicketBootstrapResponse bootstrap(@QueryParam("companyId") Long companyId, + @QueryParam("companyEntitlementId") Long companyEntitlementId) { + User user = currentUser.get(); SupportTicketViewSupport.SupportTicketCounts counts = SupportTicketViewSupport.loadTicketCounts(user); List companies = Company.list("order by name"); Company selectedCompany = selectCompany(companies, companyId); @@ -142,9 +146,8 @@ Category. list("order by name").stream().map(this::toCategoryOption).t @GET @Path("/{id}") @Transactional - public SupportTicketDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User user = requireSupport(auth); + public SupportTicketDetailResponse detail(@PathParam("id") Long id) { + User user = currentUser.get(); SupportTicketViewSupport.SupportTicketCounts counts = SupportTicketViewSupport.loadTicketCounts(user); Ticket ticket = Ticket.findById(id); if (ticket == null) { @@ -200,9 +203,7 @@ Category. list("order by name").stream().map(this::toCategoryOption).t @GET @Path("/{id}/references") @Transactional - public CrossReferenceService.CrossReferencesResponse references(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireSupport(auth); + public CrossReferenceService.CrossReferencesResponse references(@PathParam("id") Long id) { Ticket ticket = Ticket.findById(id); if (ticket == null) { throw new NotFoundException(); @@ -391,14 +392,6 @@ private String userPath(User user) { }; } - private User requireSupport(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isSupport(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - public record SupportTicketListResponse(String view, String title, int assignedCount, int openCount, String createPath, String searchTerm, List items, int page, int pageSize, int totalItems, int totalPages) { diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/SupportUserApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/SupportUserApiResource.java index fd8b85ec..1934b937 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/SupportUserApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/SupportUserApiResource.java @@ -13,8 +13,11 @@ import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotFoundException; @@ -28,14 +31,17 @@ @Path("/api/support") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("support") public class SupportUserApiResource { + @Inject + CurrentUser cUser; + @GET @Path("/users") @Transactional - public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId) { - User currentUser = requireSupport(auth); + public UserDirectoryApiModels.DirectoryListResponse list(@QueryParam("companyId") Long companyId) { + User currentUser = cUser.get(); SupportResource.SupportTicketCounts counts = SupportResource.loadTicketCounts(currentUser); List companies = Company.list("order by name"); Company selectedCompany = selectCompany(companies, companyId); @@ -53,9 +59,9 @@ public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam(AuthHelper @GET @Path("/users/bootstrap") @Transactional - public UserDirectoryApiModels.UserFormResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("countryId") Long countryId) { - User currentUser = requireSupport(auth); + public UserDirectoryApiModels.UserFormResponse bootstrap(@QueryParam("companyId") Long companyId, + @QueryParam("countryId") Long countryId) { + User currentUser = cUser.get(); SupportResource.loadTicketCounts(currentUser); List companies = Company.list("order by name"); Company selectedCompany = companyId == null ? (companies.isEmpty() ? null : companies.get(0)) @@ -86,45 +92,35 @@ public UserDirectoryApiModels.UserFormResponse bootstrap(@CookieParam(AuthHelper @GET @Path("/support-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse supportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireSupport(auth); + public UserDirectoryApiModels.UserDetailResponse supportUser(@PathParam("id") Long id) { return profileDetail(id, User.TYPE_SUPPORT, "/support/users"); } @GET @Path("/tam-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse tamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireSupport(auth); + public UserDirectoryApiModels.UserDetailResponse tamUser(@PathParam("id") Long id) { return profileDetail(id, User.TYPE_TAM, "/support/users"); } @GET @Path("/superuser-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse superuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireSupport(auth); + public UserDirectoryApiModels.UserDetailResponse superuser(@PathParam("id") Long id) { return profileDetail(id, User.TYPE_SUPERUSER, "/support/users"); } @GET @Path("/user-profiles/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse userProfile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireSupport(auth); + public UserDirectoryApiModels.UserDetailResponse userProfile(@PathParam("id") Long id) { return profileDetail(id, null, "/support/users"); } @GET @Path("/companies/{id}") @Transactional - public UserDirectoryApiModels.CompanyDetailResponse company(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireSupport(auth); + public UserDirectoryApiModels.CompanyDetailResponse company(@PathParam("id") Long id) { Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); @@ -200,11 +196,4 @@ private Country selectCountry(User user, Long countryId) { return Country.find("code", "US").firstResult(); } - private User requireSupport(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isSupport(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/TamUserApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/TamUserApiResource.java index 49b388b2..9f44cfa6 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/TamUserApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/TamUserApiResource.java @@ -13,8 +13,11 @@ import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotFoundException; @@ -27,13 +30,16 @@ @Path("/api/tam/users") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed("tam") public class TamUserApiResource { + @Inject + CurrentUser cUser; + @GET @Transactional - public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId) { - User currentUser = requireTam(auth); + public UserDirectoryApiModels.DirectoryListResponse list(@QueryParam("companyId") Long companyId) { + User currentUser = cUser.get(); List companies = Company .find("select distinct c from Company c join c.users u where u = ?1 order by c.name", currentUser) .list(); @@ -54,9 +60,9 @@ public UserDirectoryApiModels.DirectoryListResponse list(@CookieParam(AuthHelper @GET @Path("/bootstrap") @Transactional - public UserDirectoryApiModels.UserFormResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId, @QueryParam("countryId") Long countryId) { - User currentUser = requireTam(auth); + public UserDirectoryApiModels.UserFormResponse bootstrap(@QueryParam("companyId") Long companyId, + @QueryParam("countryId") Long countryId) { + User currentUser = cUser.get(); List companies = Company .find("select distinct c from Company c join c.users u where u = ?1 order by c.name", currentUser) .list(); @@ -103,12 +109,4 @@ private Country selectCountry(Long countryId) { } return Country.find("code", "US").firstResult(); } - - private User requireTam(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isUser(user) || !User.TYPE_TAM.equalsIgnoreCase(user.type)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketImportApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketImportApiResource.java index 2cb405aa..10a56dde 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketImportApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketImportApiResource.java @@ -13,11 +13,13 @@ import ai.mnemosyne_systems.service.TicketImportService; import ai.mnemosyne_systems.util.AttachmentHelper; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -32,15 +34,18 @@ @Path("/api/ticket-imports") @Produces(MediaType.APPLICATION_JSON) @Blocking +@RolesAllowed({ "admin", "support" }) public class TicketImportApiResource { + @Inject + CurrentUser currentUser; + @Inject TicketImportService ticketImportService; @GET @Path("/bootstrap") - public TicketImportBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireImportAccess(auth); + public TicketImportBootstrapResponse bootstrap() { return new TicketImportBootstrapResponse(List.of(new TicketImportFormat("csv", "CSV", CsvTicketImportSource.REQUIRED_COLUMNS, CsvTicketImportSource.OPTIONAL_COLUMNS)), "/api/ticket-imports/csv"); @@ -49,9 +54,8 @@ public TicketImportBootstrapResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOK @POST @Path("/csv") @Consumes(MediaType.MULTIPART_FORM_DATA) - public TicketImportService.TicketImportSummary importCsv(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - MultipartFormDataInput input) { - User actor = requireImportAccess(auth); + public TicketImportService.TicketImportSummary importCsv(MultipartFormDataInput input) { + User actor = currentUser.get(); AttachmentHelper.UploadedFile file = AttachmentHelper.readFile(input, "file", CsvTicketImportSource.MAX_CSV_BYTES); if (file == null || file.data() == null || file.data().length == 0) { @@ -65,14 +69,6 @@ public TicketImportService.TicketImportSummary importCsv(@CookieParam(AuthHelper } } - private User requireImportAccess(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isSupport(user) && !AuthHelper.isAdmin(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - public record TicketImportBootstrapResponse(List formats, String submitPath) { } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketRatingApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketRatingApiResource.java index 73ee9601..28f825af 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketRatingApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketRatingApiResource.java @@ -10,12 +10,13 @@ import ai.mnemosyne_systems.model.Ticket; import ai.mnemosyne_systems.model.User; -import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.NotFoundException; @@ -36,17 +37,17 @@ public class TicketRatingApiResource { private static final int MAX_COMMENT_LENGTH = 2000; + @Inject + CurrentUser currentUser; + @POST @Path("/{id}/rating") @Transactional - public Response submitRating(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id, + @RolesAllowed({ "user", "superuser" }) + public Response submitRating(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id, @FormParam("rating") Integer rating, @FormParam("ratingComment") String ratingComment) { - User user = AuthHelper.findUser(auth); - if (user == null || (!User.TYPE_USER.equalsIgnoreCase(user.type) && !AuthHelper.isSuperuser(user))) { - throw new WebApplicationException(Response.seeOther(URI.create("/login")).build()); - } + User user = currentUser.get(); Ticket ticket = Ticket.findById(id); if (ticket == null) { diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketResource.java index d26cb1d2..9364ddd7 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketResource.java @@ -19,12 +19,14 @@ import ai.mnemosyne_systems.service.PdfService; import ai.mnemosyne_systems.service.TicketEmailService; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -59,6 +61,7 @@ @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class TicketResource { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d yyyy, h.mma", @@ -66,6 +69,9 @@ public class TicketResource { private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL) .connectTimeout(Duration.ofSeconds(10)).build(); + @Inject + CurrentUser currentUser; + @Inject SupportResource supportResource; @@ -85,22 +91,22 @@ public class TicketResource { EventService eventService; @GET - public Response list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSupport(auth); + @RolesAllowed("support") + public Response list() { return Response.seeOther(URI.create("/tickets")).build(); } @GET @Path("/new") - public Response createForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireSupport(auth); + @RolesAllowed("support") + public Response createForm() { return Response.seeOther(URI.create("/tickets/new")).build(); } @GET @Path("/{id}/edit") - public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSupport(auth); + @RolesAllowed("support") + public Response editForm(@PathParam("id") Long id) { Ticket ticket = Ticket.find( "select t from Ticket t left join fetch t.companyEntitlement ce left join fetch ce.entitlement left join fetch ce.supportLevel where t.id = ?1", id).firstResult(); @@ -112,16 +118,16 @@ public Response editForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path @GET @Path("/{id}") - public Object viewTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = AuthHelper.findUser(auth); + public Object viewTicket(@PathParam("id") Long id) { + User user = currentUser.get(); if (AuthHelper.isSupport(user)) { - return supportResource.ticketDetail(auth, id); + return supportResource.ticketDetail(id); } if (AuthHelper.isSuperuser(user)) { - return superuserResource.ticketDetail(auth, id); + return superuserResource.ticketDetail(id); } if (AuthHelper.isUser(user)) { - return userResource.ticketDetail(auth, id); + return userResource.ticketDetail(id); } return Response.seeOther(URI.create("/")).build(); } @@ -129,16 +135,16 @@ public Object viewTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Path @GET @Path("/alarm/status") @Produces(MediaType.TEXT_PLAIN) - public Response alarmStatus(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - User user = AuthHelper.findUser(auth); + public Response alarmStatus() { + User user = currentUser.get(); boolean alarm = hasAlarm(user); return Response.ok(Boolean.toString(alarm)).build(); } @GET @Path("/external-preview") - public Response externalPreview(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryParam("url") String url) { - requireSupport(auth); + @RolesAllowed("support") + public Response externalPreview(@QueryParam("url") String url) { if (url == null || url.isBlank()) { throw new BadRequestException("URL is required"); } @@ -158,11 +164,11 @@ public Response externalPreview(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @POST @Transactional - public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("status") String status, + @RolesAllowed("support") + public Response create(@HeaderParam("X-Billetsys-Client") String client, @FormParam("status") String status, @FormParam("title") String title, @FormParam("companyId") Long companyId, @FormParam("companyEntitlementId") Long companyEntitlementId, @FormParam("categoryId") Long categoryId) { - User user = requireSupport(auth); + User user = currentUser.get(); String normalizedTitle = Ticket.normalizeTitle(title); if (status == null || status.isBlank()) { throw new BadRequestException("Status is required"); @@ -203,13 +209,14 @@ public Response create(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @POST @Path("/{id}") @Transactional - public Response update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("status") String status, - @FormParam("companyId") Long companyId, @FormParam("companyEntitlementId") Long companyEntitlementId, - @FormParam("categoryId") Long categoryId, @FormParam("externalIssueLink") String externalIssueLink, + @RolesAllowed("support") + public Response update(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + @FormParam("status") String status, @FormParam("companyId") Long companyId, + @FormParam("companyEntitlementId") Long companyEntitlementId, @FormParam("categoryId") Long categoryId, + @FormParam("externalIssueLink") String externalIssueLink, @FormParam("affectsVersionId") Long affectsVersionId, @FormParam("resolvedVersionId") Long resolvedVersionId) { - User user = requireSupport(auth); + User user = currentUser.get(); Ticket ticket = Ticket.findById(id); if (ticket == null) { throw new NotFoundException(); @@ -261,13 +268,13 @@ public Response update(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathPa @POST @Path("/{id}/delete") @Transactional - public Response delete(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { - User user = requireSupport(auth); + @RolesAllowed("support") + public Response delete(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { Ticket ticket = Ticket.findById(id); if (ticket == null) { throw new NotFoundException(); } + User user = currentUser.get(); eventService.recordTicketDeleted(ticket, user); ticket.delete(); return ReactRedirectSupport.redirect(client, "/tickets"); @@ -275,8 +282,8 @@ public Response delete(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("/company/{id}/entitlements") - public Response companyEntitlements(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireSupport(auth); + @RolesAllowed("support") + public Response companyEntitlements(@PathParam("id") Long id) { Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); @@ -325,8 +332,8 @@ private Version resolveVersion(CompanyEntitlement companyEntitlement, Long versi @GET @Path("/export/{id}") @Produces("application/pdf") - public Response exportTicketToPdf(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = AuthHelper.findUser(auth); + public Response exportTicketToPdf(@PathParam("id") Long id) { + User user = currentUser.get(); if (!AuthHelper.isSupport(user) && !AuthHelper.isUser(user) && !AuthHelper.isSuperuser(user)) { throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); } @@ -343,14 +350,6 @@ public Response exportTicketToPdf(@CookieParam(AuthHelper.AUTH_COOKIE) String au .build(); } - private User requireSupport(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isSupport(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - private String fetchExternalPreviewHtml(URI previewUri) { HttpRequest request = HttpRequest.newBuilder(previewUri).timeout(Duration.ofSeconds(15)) .header("User-Agent", "billetsys-preview").GET().build(); diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketWorkbenchApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketWorkbenchApiResource.java index 9b9be76f..67de73f6 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/TicketWorkbenchApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/TicketWorkbenchApiResource.java @@ -16,9 +16,12 @@ import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.model.Version; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; + import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; @@ -39,15 +42,19 @@ @Path("/api/ticket-workbench") @Produces(MediaType.APPLICATION_JSON) @Blocking +@RolesAllowed("support") public class TicketWorkbenchApiResource { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d yyyy, h.mma", Locale.ENGLISH); + @Inject + CurrentUser currentUser; + @GET @Transactional - public TicketListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - User user = requireSupport(auth); + public TicketListResponse list() { + User user = currentUser.get(); List tickets = Ticket.listAll(); Map lastMessageLabels = new LinkedHashMap<>(); for (Message message : MessageVisibilitySupport @@ -69,9 +76,9 @@ public TicketListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) @GET @Path("/bootstrap") @Transactional - public TicketFormResponse bootstrap(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("ticketId") Long ticketId, @QueryParam("companyId") Long companyId) { - User user = requireSupport(auth); + public TicketFormResponse bootstrap(@QueryParam("ticketId") Long ticketId, + @QueryParam("companyId") Long companyId) { + User user = currentUser.get(); Ticket ticket = ticketId == null ? new Ticket() : Ticket.find( "select t from Ticket t left join fetch t.companyEntitlement ce left join fetch ce.entitlement left join fetch ce.supportLevel where t.id = ?1", @@ -117,8 +124,8 @@ Category. listAll().stream().map(category -> new CategoryOption(catego @GET @Path("/{id}") @Transactional - public TicketFormResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - return bootstrap(auth, id, null); + public TicketFormResponse detail(@PathParam("id") Long id) { + return bootstrap(id, null); } private MessageSummary toMessageSummary(Message message) { @@ -184,14 +191,6 @@ private String formatDate(LocalDateTime date) { return date == null ? "-" : DATE_FORMATTER.format(date); } - private User requireSupport(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isSupport(user)) { - throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); - } - return user; - } - public record TicketListResponse(String title, String createPath, List items) { } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/TimezoneResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/TimezoneResource.java index ec664f7d..1b267035 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/TimezoneResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/TimezoneResource.java @@ -11,9 +11,10 @@ import ai.mnemosyne_systems.model.Country; import ai.mnemosyne_systems.model.Timezone; import ai.mnemosyne_systems.model.User; -import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; import io.smallrye.common.annotation.Blocking; -import jakarta.ws.rs.CookieParam; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -25,14 +26,16 @@ @Path("/timezones") @Produces(MediaType.APPLICATION_JSON) @Blocking +@RolesAllowed({ "admin", "support", "superuser", "tam", "user" }) public class TimezoneResource { + @Inject + CurrentUser currentUser; + @GET - public Response list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryParam("countryId") Long countryId) { - User user = AuthHelper.findUser(auth); - if (user == null) { - return Response.status(Response.Status.UNAUTHORIZED).build(); - } + public Response list(@QueryParam("countryId") Long countryId) { + User user = currentUser.get(); + if (countryId == null) { return Response.ok("[]").build(); } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/UserResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/UserResource.java index 8f8f3ece..f7db9ec9 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/UserResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/UserResource.java @@ -24,6 +24,7 @@ import ai.mnemosyne_systems.util.AttachmentHelper; import ai.mnemosyne_systems.util.AuthHelper; import ai.mnemosyne_systems.util.TicketTimeSupport; +import ai.mnemosyne_systems.util.CurrentUser; import io.quarkus.hibernate.orm.panache.Panache; import io.quarkus.elytron.security.common.BcryptUtil; import io.smallrye.common.annotation.Blocking; @@ -31,7 +32,6 @@ import jakarta.transaction.Transactional; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -55,6 +55,10 @@ @Produces(MediaType.TEXT_HTML) @Blocking public class UserResource { + + @Inject + CurrentUser currentUser; + @Inject CrossReferenceService crossReferenceService; @@ -66,19 +70,19 @@ public class UserResource { @GET @Path("user") - public Object home(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - User user = AuthHelper.findUser(auth); + public Object home() { + User user = currentUser.get(); if (AuthHelper.isSuperuser(user)) { return Response.seeOther(URI.create("/superuser")).build(); } - requireUser(auth); + requireUser(); return Response.seeOther(URI.create("/user/tickets")).build(); } @GET @Path("tam/users") - public Response tamUsersRoot(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - User user = requireUser(auth); + public Response tamUsersRoot() { + User user = requireUser(); if (!User.TYPE_TAM.equalsIgnoreCase(user.type)) { throw new WebApplicationException(Response.seeOther(URI.create("/user")).build()); } @@ -87,9 +91,8 @@ public Response tamUsersRoot(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { @GET @Path("tam/users/{companyId}") - public Response listTamUsers(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("companyId") Long companyId) { - User user = requireUser(auth); + public Response listTamUsers(@PathParam("companyId") Long companyId) { + User user = requireUser(); if (!User.TYPE_TAM.equalsIgnoreCase(user.type)) { throw new WebApplicationException(Response.seeOther(URI.create("/user")).build()); } @@ -107,9 +110,8 @@ public Response listTamUsers(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("tam/users/{companyId}/create") - public Response createTamUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("companyId") Long companyId) { - User user = requireUser(auth); + public Response createTamUserForm(@PathParam("companyId") Long companyId) { + User user = requireUser(); if (!User.TYPE_TAM.equalsIgnoreCase(user.type)) { throw new WebApplicationException(Response.seeOther(URI.create("/user")).build()); } @@ -128,13 +130,13 @@ public Response createTamUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String au @POST @Path("tam/users") @Transactional - public Response createTamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @FormParam("name") String name, - @FormParam("fullName") String fullName, @FormParam("email") String email, - @FormParam("social") String social, @FormParam("phoneNumber") String phoneNumber, - @FormParam("phoneExtension") String phoneExtension, @FormParam("timezoneId") Long timezoneId, - @FormParam("countryId") Long countryId, @FormParam("password") String password, - @FormParam("type") String type, @FormParam("companyId") Long companyId) { - User user = requireUser(auth); + public Response createTamUser(@FormParam("name") String name, @FormParam("fullName") String fullName, + @FormParam("email") String email, @FormParam("social") String social, + @FormParam("phoneNumber") String phoneNumber, @FormParam("phoneExtension") String phoneExtension, + @FormParam("timezoneId") Long timezoneId, @FormParam("countryId") Long countryId, + @FormParam("password") String password, @FormParam("type") String type, + @FormParam("companyId") Long companyId) { + User user = requireUser(); if (!User.TYPE_TAM.equalsIgnoreCase(user.type)) { throw new WebApplicationException(Response.seeOther(URI.create("/user")).build()); } @@ -179,7 +181,7 @@ public Response createTamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, } newUser.persist(); eventService.record(newUser.id, ai.mnemosyne_systems.model.event.EventConstants.USER_CREATED, company.id, - AuthHelper.findUser(auth).id, "User created"); + user.id, "User created"); boolean exists = company.users.stream() .anyMatch(existing -> existing.id != null && existing.id.equals(newUser.id)); if (!exists) { @@ -190,15 +192,15 @@ public Response createTamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("user/tickets") - public Response tickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireUser(auth); + public Response tickets() { + requireUser(); return Response.seeOther(URI.create("/user/tickets")).build(); } @GET @Path("user/tickets/create") - public Response createTicketForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireUser(auth); + public Response createTicketForm() { + requireUser(); return Response.seeOther(URI.create("/user/tickets/new")).build(); } @@ -206,9 +208,8 @@ public Response createTicketForm(@CookieParam(AuthHelper.AUTH_COOKIE) String aut @Path("user/tickets") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response createUserTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { - User user = requireUser(auth); + public Response createUserTicket(@HeaderParam("X-Billetsys-Client") String client, MultipartFormDataInput input) { + User user = requireUser(); String status = AttachmentHelper.readFormValue(input, "status"); String title = Ticket.normalizeTitle(AttachmentHelper.readFormValue(input, "title")); String messageBody = AttachmentHelper.readFormValue(input, "message"); @@ -271,8 +272,8 @@ public Response createUserTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String aut @GET @Path("user/support-users/{id}") - public Response viewSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public Response viewSupportUser(@PathParam("id") Long id) { + User user = requireUser(); User supportUser = User.findById(id); if (supportUser == null || !User.TYPE_SUPPORT.equalsIgnoreCase(supportUser.type)) { throw new NotFoundException(); @@ -282,8 +283,8 @@ public Response viewSupportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("user/tam-users/{id}") - public Response viewTamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public Response viewTamUser(@PathParam("id") Long id) { + User user = requireUser(); User tamUser = User.findById(id); if (tamUser == null || !User.TYPE_TAM.equalsIgnoreCase(tamUser.type)) { throw new NotFoundException(); @@ -293,8 +294,8 @@ public Response viewTamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @P @GET @Path("user/superuser-users/{id}") - public Response viewSuperuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public Response viewSuperuser(@PathParam("id") Long id) { + User user = requireUser(); User superuser = User.findById(id); if (superuser == null || !User.TYPE_SUPERUSER.equalsIgnoreCase(superuser.type)) { throw new NotFoundException(); @@ -310,8 +311,8 @@ public Response viewSuperuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("user/user-profiles/{id}") - public Response viewUserProfile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public Response viewUserProfile(@PathParam("id") Long id) { + User user = requireUser(); User viewedUser = User.findById(id); if (viewedUser == null) { throw new NotFoundException(); @@ -321,8 +322,8 @@ public Response viewUserProfile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @GET @Path("user/companies/{id}") - public Response viewCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public Response viewCompany(@PathParam("id") Long id) { + User user = requireUser(); Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); @@ -332,22 +333,22 @@ public Response viewCompany(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @P @GET @Path("user/tickets/open") - public Response tamOpenTickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireUser(auth); + public Response tamOpenTickets() { + requireUser(); return Response.seeOther(URI.create("/user/tickets/open")).build(); } @GET @Path("user/tickets/closed") - public Response tamClosedTickets(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireUser(auth); + public Response tamClosedTickets() { + requireUser(); return Response.seeOther(URI.create("/user/tickets/closed")).build(); } @GET @Path("user/tickets/{id}") - public Response ticketDetail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public Response ticketDetail(@PathParam("id") Long id) { + User user = requireUser(); if (findTicketForUser(user, id) == null) { throw new NotFoundException(); } @@ -362,9 +363,8 @@ private List loadMessages(Ticket ticket, Use @Path("user/tickets/{id}/messages") @Consumes(MediaType.MULTIPART_FORM_DATA) @Transactional - public Response addUserMessage(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - MultipartFormDataInput input) { - User user = requireUser(auth); + public Response addUserMessage(@PathParam("id") Long id, MultipartFormDataInput input) { + User user = requireUser(); String body = AttachmentHelper.readFormValue(input, "body"); if (body == null || body.isBlank()) { throw new BadRequestException("Message is required"); @@ -396,8 +396,8 @@ public Response addUserMessage(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("user/tickets/{id}/edit") - public Response ticketEdit(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public Response ticketEdit(@PathParam("id") Long id) { + User user = requireUser(); if (findTicketForUser(user, id) == null) { throw new NotFoundException(); } @@ -407,11 +407,10 @@ public Response ticketEdit(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @Pa @POST @Path("user/tickets/{id}") @Transactional - public Response updateUserTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("title") String title, - @FormParam("affectsVersionId") Long affectsVersionId, + public Response updateUserTicket(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + @FormParam("title") String title, @FormParam("affectsVersionId") Long affectsVersionId, @FormParam("resolvedVersionId") Long resolvedVersionId) { - User user = requireUser(auth); + User user = requireUser(); String normalizedTitle = Ticket.normalizeTitle(title); Ticket ticket = findTicketForUser(user, id); if (ticket == null) { @@ -431,9 +430,8 @@ public Response updateUserTicket(@CookieParam(AuthHelper.AUTH_COOKIE) String aut @GET @Path("users") - public Response listAdminUsersRoot(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @QueryParam("companyId") Long companyId) { - requireAdmin(auth); + public Response listAdminUsersRoot(@QueryParam("companyId") Long companyId) { + requireAdmin(); if (companyId != null) { return Response.seeOther(URI.create("/users?companyId=" + companyId)).build(); } @@ -442,9 +440,8 @@ public Response listAdminUsersRoot(@CookieParam(AuthHelper.AUTH_COOKIE) String a @GET @Path("users/{companyId}") - public Response listAdminUsers(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("companyId") Long companyId) { - requireAdmin(auth); + public Response listAdminUsers(@PathParam("companyId") Long companyId) { + requireAdmin(); Company selectedCompany = Company.findById(companyId); if (selectedCompany == null) { throw new NotFoundException(); @@ -454,15 +451,15 @@ public Response listAdminUsers(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @GET @Path("users/create") - public Response createAdminUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth) { - requireAdmin(auth); + public Response createAdminUserForm() { + requireAdmin(); return Response.seeOther(URI.create("/users/new")).build(); } @GET @Path("user/{id}/edit") - public Response editAdminUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response editAdminUserForm(@PathParam("id") Long id) { + requireAdmin(); User editUser = User.findById(id); if (editUser == null) { throw new NotFoundException(); @@ -472,8 +469,8 @@ public Response editAdminUserForm(@CookieParam(AuthHelper.AUTH_COOKIE) String au @GET @Path("user/{id}") - public Response viewAdminUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - requireAdmin(auth); + public Response viewAdminUser(@PathParam("id") Long id) { + requireAdmin(); User viewUser = User.findById(id); if (viewUser == null) { throw new NotFoundException(); @@ -484,14 +481,13 @@ public Response viewAdminUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @POST @Path("users") @Transactional - public Response createAdminUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, + public Response createAdminUser(@HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, @FormParam("fullName") String fullName, @FormParam("email") String email, @FormParam("social") String social, @FormParam("phoneNumber") String phoneNumber, @FormParam("phoneExtension") String phoneExtension, @FormParam("timezoneId") Long timezoneId, @FormParam("countryId") Long countryId, @FormParam("type") String type, @FormParam("password") String password, @FormParam("companyId") Long companyId) { - requireAdmin(auth); + User user = requireAdmin(); validateUserFields(name, email, type, true, password); User newUser = new User(); newUser.name = name.trim(); @@ -509,8 +505,8 @@ public Response createAdminUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth newUser.passwordHash = BcryptUtil.bcryptHash(password); } newUser.persist(); - eventService.record(newUser.id, ai.mnemosyne_systems.model.event.EventConstants.USER_CREATED, null, - AuthHelper.findUser(auth).id, "User created"); + eventService.record(newUser.id, ai.mnemosyne_systems.model.event.EventConstants.USER_CREATED, null, user.id, + "User created"); if (companyId != null) { Company company = Company.findById(companyId); if (company != null) { @@ -523,14 +519,13 @@ public Response createAdminUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth @POST @Path("user/{id}") @Transactional - public Response updateAdminUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id, - @HeaderParam("X-Billetsys-Client") String client, @FormParam("name") String name, - @FormParam("fullName") String fullName, @FormParam("email") String email, + public Response updateAdminUser(@PathParam("id") Long id, @HeaderParam("X-Billetsys-Client") String client, + @FormParam("name") String name, @FormParam("fullName") String fullName, @FormParam("email") String email, @FormParam("social") String social, @FormParam("phoneNumber") String phoneNumber, @FormParam("phoneExtension") String phoneExtension, @FormParam("timezoneId") Long timezoneId, @FormParam("countryId") Long countryId, @FormParam("type") String type, @FormParam("password") String password, @FormParam("companyId") Long companyId) { - requireAdmin(auth); + requireAdmin(); User editUser = User.findById(id); if (editUser == null) { throw new NotFoundException(); @@ -576,16 +571,15 @@ private String trimOrNull(String value) { @POST @Path("user/{id}/delete") @Transactional - public Response deleteAdminUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { - requireAdmin(auth); + public Response deleteAdminUser(@HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id) { + User user = requireAdmin(); User deleteUser = User.findById(id); if (deleteUser == null) { throw new NotFoundException(); } removeUserReferences(deleteUser); - eventService.record(deleteUser.id, ai.mnemosyne_systems.model.event.EventConstants.USER_DELETED, null, - AuthHelper.findUser(auth).id, "User deleted"); + eventService.record(deleteUser.id, ai.mnemosyne_systems.model.event.EventConstants.USER_DELETED, null, user.id, + "User deleted"); deleteUser.delete(); return ReactRedirectSupport.redirect(client, "/users"); } @@ -1095,16 +1089,16 @@ private String typeLabel(String type) { }; } - private User requireUser(String auth) { - User user = AuthHelper.findUser(auth); + private User requireUser() { + User user = currentUser.get(); if (!AuthHelper.isUser(user)) { throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); } return user; } - private User requireAdmin(String auth) { - User user = AuthHelper.findUser(auth); + private User requireAdmin() { + User user = currentUser.get(); if (!AuthHelper.isAdmin(user)) { throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); } diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/UserTicketApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/UserTicketApiResource.java index 51d91de8..0e1fa7e4 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/UserTicketApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/UserTicketApiResource.java @@ -20,9 +20,10 @@ import ai.mnemosyne_systems.service.CrossReferenceService; import ai.mnemosyne_systems.service.EventService; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; @@ -41,8 +42,12 @@ @Path("/api/user/tickets") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed({ "tam", "user" }) public class UserTicketApiResource { + @Inject + CurrentUser currentUser; + @Inject CrossReferenceService crossReferenceService; @@ -54,11 +59,11 @@ public class UserTicketApiResource { @GET @Transactional - public SupportTicketApiResource.SupportTicketListResponse list(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, + public SupportTicketApiResource.SupportTicketListResponse list( @QueryParam("view") @DefaultValue("assigned") String view, @QueryParam("q") String q, @QueryParam("page") Integer page, @QueryParam("pageSize") Integer pageSize, @QueryParam("sort") String sort, @QueryParam("dir") String dir) { - User user = requireUser(auth); + User user = currentUser.get(); UserResource.SupportTicketData data = userResource.buildTicketDataForUser(user); String normalizedView = normalizeView(view); List tickets = switch (normalizedView) { @@ -91,10 +96,10 @@ public SupportTicketApiResource.SupportTicketListResponse list(@CookieParam(Auth @GET @Path("/suggest") @Transactional - public SupportTicketApiResource.TicketSuggestionResponse suggest(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, + public SupportTicketApiResource.TicketSuggestionResponse suggest( @QueryParam("view") @DefaultValue("assigned") String view, @QueryParam("q") String q, @QueryParam("exclude") Long exclude) { - User user = requireUser(auth); + User user = currentUser.get(); UserResource.SupportTicketData data = userResource.buildTicketDataForUser(user); List tickets = TicketSearchSupport.combineTickets(data.assignedTickets, data.openTickets, data.closedTickets); @@ -110,10 +115,9 @@ public SupportTicketApiResource.TicketSuggestionResponse suggest(@CookieParam(Au @GET @Path("/bootstrap") @Transactional - public SupportTicketApiResource.SupportTicketBootstrapResponse bootstrap( - @CookieParam(AuthHelper.AUTH_COOKIE) String auth, @QueryParam("companyId") Long companyId, + public SupportTicketApiResource.SupportTicketBootstrapResponse bootstrap(@QueryParam("companyId") Long companyId, @QueryParam("companyEntitlementId") Long companyEntitlementId) { - User user = requireUser(auth); + User user = currentUser.get(); UserResource.SupportTicketData data = userResource.buildTicketDataForUser(user); List companies = Company .find("select distinct c from Company c join c.users u where u = ?1 order by c.name", user).list(); @@ -151,8 +155,8 @@ Category. list("order by name").stream().map(this::toCategoryOption).t @GET @Path("/{id}") @Transactional - public RoleTicketDetailResponse detail(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, @PathParam("id") Long id) { - User user = requireUser(auth); + public RoleTicketDetailResponse detail(@PathParam("id") Long id) { + User user = currentUser.get(); Ticket ticket = userResource.findTicketForUser(user, id); if (ticket == null) { throw new NotFoundException(); @@ -215,9 +219,8 @@ Category. list("order by name").stream().map(this::toCategoryOption).t @GET @Path("/{id}/references") @Transactional - public CrossReferenceService.CrossReferencesResponse references(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User user = requireUser(auth); + public CrossReferenceService.CrossReferencesResponse references(@PathParam("id") Long id) { + User user = currentUser.get(); Ticket ticket = userResource.findTicketForUser(user, id); if (ticket == null) { throw new NotFoundException(); @@ -372,14 +375,6 @@ private String userPath(User user) { }; } - private User requireUser(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isUser(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } - public record RoleTicketDetailResponse(Long id, String name, String title, String displayStatus, int assignedCount, int openCount, Long companyId, String companyName, Long categoryId, String categoryName, Long companyEntitlementId, String entitlementName, String levelName, String externalIssueLink, diff --git a/src/backend/main/java/ai/mnemosyne_systems/resource/UserViewApiResource.java b/src/backend/main/java/ai/mnemosyne_systems/resource/UserViewApiResource.java index 83102404..8b5d2a0c 100644 --- a/src/backend/main/java/ai/mnemosyne_systems/resource/UserViewApiResource.java +++ b/src/backend/main/java/ai/mnemosyne_systems/resource/UserViewApiResource.java @@ -11,8 +11,10 @@ import ai.mnemosyne_systems.model.Company; import ai.mnemosyne_systems.model.User; import ai.mnemosyne_systems.util.AuthHelper; +import ai.mnemosyne_systems.util.CurrentUser; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotFoundException; @@ -25,39 +27,38 @@ @Path("/api/user") @Produces(MediaType.APPLICATION_JSON) +@RolesAllowed({ "tam", "user" }) public class UserViewApiResource { + @Inject + CurrentUser currentUser; + @GET @Path("/support-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse supportUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireUser(auth); + public UserDirectoryApiModels.UserDetailResponse supportUser(@PathParam("id") Long id) { return profileDetail(id, User.TYPE_SUPPORT); } @GET @Path("/tam-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse tamUser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireUser(auth); + public UserDirectoryApiModels.UserDetailResponse tamUser(@PathParam("id") Long id) { return profileDetail(id, User.TYPE_TAM); } @GET @Path("/superuser-users/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse superuser(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - User currentUser = requireUser(auth); + public UserDirectoryApiModels.UserDetailResponse superuser(@PathParam("id") Long id) { + User cUser = currentUser.get(); User user = User.findById(id); if (user == null || !User.TYPE_SUPERUSER.equalsIgnoreCase(user.type)) { throw new NotFoundException(); } boolean allowed = Company.count( "select count(c) from Company c join c.users current join c.users viewed where current = ?1 and viewed = ?2", - currentUser, user) > 0; + cUser, user) > 0; if (!allowed) { throw new NotFoundException(); } @@ -67,9 +68,7 @@ public UserDirectoryApiModels.UserDetailResponse superuser(@CookieParam(AuthHelp @GET @Path("/user-profiles/{id}") @Transactional - public UserDirectoryApiModels.UserDetailResponse profile(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireUser(auth); + public UserDirectoryApiModels.UserDetailResponse profile(@PathParam("id") Long id) { User user = User.findById(id); if (user == null) { throw new NotFoundException(); @@ -80,9 +79,7 @@ public UserDirectoryApiModels.UserDetailResponse profile(@CookieParam(AuthHelper @GET @Path("/companies/{id}") @Transactional - public UserDirectoryApiModels.CompanyDetailResponse company(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, - @PathParam("id") Long id) { - requireUser(auth); + public UserDirectoryApiModels.CompanyDetailResponse company(@PathParam("id") Long id) { Company company = Company.findById(id); if (company == null) { throw new NotFoundException(); @@ -122,11 +119,4 @@ private List usersForCompany(Company compa .map(user -> UserDirectoryApiModels.userReference(user, basePath + user.id, null)).toList(); } - private User requireUser(String auth) { - User user = AuthHelper.findUser(auth); - if (!AuthHelper.isUser(user)) { - throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); - } - return user; - } } diff --git a/src/backend/main/java/ai/mnemosyne_systems/service/UserProvisioningService.java b/src/backend/main/java/ai/mnemosyne_systems/service/UserProvisioningService.java new file mode 100644 index 00000000..5528a71a --- /dev/null +++ b/src/backend/main/java/ai/mnemosyne_systems/service/UserProvisioningService.java @@ -0,0 +1,75 @@ +/* + * Eclipse Public License - v 2.0 + * + * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + * PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + */ + +package ai.mnemosyne_systems.service; + +import ai.mnemosyne_systems.model.User; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.json.JsonString; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.List; +import java.util.Map; + +@ApplicationScoped +public class UserProvisioningService { + @Transactional + public User getOrCreateUser(JsonWebToken jwt) { + + String keycloakId = jwt.getSubject(); // "sub" + String email = jwt.getClaim("email"); + String username = jwt.getClaim("preferred_username"); + String fullName = jwt.getClaim("name"); + jakarta.json.JsonObject realmAccess = jwt.getClaim("realm_access"); + List roles = realmAccess != null + ? realmAccess.getJsonArray("roles").getValuesAs(JsonString.class).stream().map(JsonString::getString) + .toList() + : List.of(); + + // look up by keycloakId first, fall back to email for existing + // users that were created before keycloakId column was added + User user = User.find("keycloakId", keycloakId).firstResult(); + if (user == null && email != null) { + user = User.find("email", email).firstResult(); + } + + if (user != null) { + // sync fields that may have changed in Keycloak + user.keycloakId = keycloakId; // stamp keycloakId if missing + user.email = email; + user.name = username; + user.fullName = fullName; + user.type = resolveType(roles); + return user; + } + + // JIT provision new user + user = new User(); + user.keycloakId = keycloakId; + user.email = email; + user.name = username; + user.fullName = fullName; + user.type = resolveType(roles); + + user.persist(); + return user; + } + + private String resolveType(List roles) { + if (roles.contains("admin")) + return User.TYPE_ADMIN; + if (roles.contains("support")) + return User.TYPE_SUPPORT; + if (roles.contains("tam")) + return User.TYPE_TAM; + if (roles.contains("superuser")) + return User.TYPE_SUPERUSER; + return User.TYPE_USER; + } +} diff --git a/src/backend/main/java/ai/mnemosyne_systems/util/CurrentUser.java b/src/backend/main/java/ai/mnemosyne_systems/util/CurrentUser.java new file mode 100644 index 00000000..42494230 --- /dev/null +++ b/src/backend/main/java/ai/mnemosyne_systems/util/CurrentUser.java @@ -0,0 +1,92 @@ +/* + * Eclipse Public License - v 2.0 + * + * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + * PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + */ + +package ai.mnemosyne_systems.util; + +import ai.mnemosyne_systems.model.User; +import ai.mnemosyne_systems.service.UserProvisioningService; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; +import org.eclipse.microprofile.jwt.JsonWebToken; + +@RequestScoped +public class CurrentUser { + + @Inject + SecurityIdentity identity; + + @Inject + Instance jwt; + + @Inject + UserProvisioningService provisioningService; + + private User resolved; + private boolean loaded; + + /** + * Returns the User matching the current Keycloak principal. Result is cached per-request so the DB is hit at most + * once. + * + * @throws NotFoundException + * if the token is valid but no matching User exists in the database. + */ + public User get() { + if (!loaded) { + resolved = load(); + loaded = true; + } + return resolved; + } + + /** + * Same as get() but returns null instead of throwing when the user is not found — useful for optional identity + * checks. + */ + public User getOrNull() { + if (!loaded) { + resolved = load(); + loaded = true; + } + return resolved; + } + + public boolean isAnonymous() { + return identity == null || identity.isAnonymous(); + } + + private User load() { + if (isAnonymous()) { + return null; + } + + // Production path — real Keycloak JWT + if (!jwt.isUnsatisfied()) { + try { + JsonWebToken token = jwt.get(); + if (token.getSubject() != null && !token.getSubject().isBlank()) { + return provisioningService.getOrCreateUser(token); + } + } catch (Exception ignored) { + // fall through to principal-name lookup + } + } + + // Test path — @TestSecurity sets principal name to the user() value + // and @JwtSecurity injects claims including email + String principal = identity.getPrincipal().getName(); + User user = User.find("email", principal).firstResult(); + if (user == null) { + user = User.find("name", principal).firstResult(); + } + return user; + } +} diff --git a/src/backend/main/resources/application.properties b/src/backend/main/resources/application.properties index 9aa43f41..7c3e0f8c 100644 --- a/src/backend/main/resources/application.properties +++ b/src/backend/main/resources/application.properties @@ -62,3 +62,33 @@ app.public-base-url=${APP_PUBLIC_BASE_URL:} cap.api.endpoint=${CAP_API_ENDPOINT:} cap.siteverify.url=${CAP_SITEVERIFY_URL:} cap.secret.key=${CAP_SECRET_KEY:} + + +# OIDC Settings + +quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://localhost:8180}/realms/${KEYCLOAK_REALM_NAME:billetsys} +quarkus.oidc.client-id=${KEYCLOAK_BACKEND_CLIENT_ID:billetsys-backend} +quarkus.oidc.credentials.secret=${KEYCLOAK_BACKEND_CLIENT_SECRET:123456} +quarkus.oidc.application-type=service +quarkus.oidc.authentication.scopes=openid,profile,email +quarkus.oidc.roles.role-claim-path=realm_access/roles +quarkus.oidc.token.principal-claim=sub +quarkus.oidc.roles.source=accesstoken + +# Security Configuration +quarkus.security.users.embedded.enabled=false + +# Public endpoints (no authentication required) +quarkus.http.auth.permission.public.paths=/api/public/*,/api/app/session,/api/cap/*,/health/*,/metrics +quarkus.http.auth.permission.public.policy=permit +quarkus.http.auth.permission.public.priority=1 + +# Authenticated endpoints +quarkus.http.auth.permission.authenticated.paths=/api/* +quarkus.http.auth.permission.authenticated.policy=authenticated + +quarkus.http.auth.permission.authenticated.priority=2 + +quarkus.log.category."io.quarkus.oidc".level=DEBUG +quarkus.log.category."io.quarkus.security".level=DEBUG +quarkus.log.level=INFO diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/AccessTestSupport.java b/src/backend/test/java/ai/mnemosyne_systems/resource/AccessTestSupport.java index 39cec76f..985b0d39 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/AccessTestSupport.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/AccessTestSupport.java @@ -24,8 +24,6 @@ import ai.mnemosyne_systems.model.Version; import ai.mnemosyne_systems.service.MailboxPollingService; import ai.mnemosyne_systems.service.TicketEmailService; -import ai.mnemosyne_systems.util.AuthHelper; -import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.hibernate.orm.panache.Panache; import io.quarkus.mailer.Mail; import io.quarkus.mailer.MockMailbox; @@ -59,20 +57,18 @@ abstract class AccessTestSupport { TicketEmailService ticketEmailService; @Transactional - void ensureUser(String name, String email, String type, String password) { + void ensureUser(String name, String email, String type) { User user = User.find("email", email).firstResult(); if (user == null) { user = new User(); user.name = name; user.email = email; user.type = type; - user.passwordHash = BcryptUtil.bcryptHash(password); user.persist(); return; } user.name = name; user.type = type; - user.passwordHash = BcryptUtil.bcryptHash(password); } @Transactional @@ -273,11 +269,11 @@ void ensureDefaultCategories() { question.persist(); } - String login(String username, String password) { - return RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC) - .formParam("username", username).formParam("password", password).post("/login").then().statusCode(303) - .extract().cookie(AuthHelper.AUTH_COOKIE); - } + // String login(String username, String password) { + // return RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC) + // .formParam("username", username).formParam("password", password).post("/login").then().statusCode(303) + // .extract().cookie(AuthHelper.AUTH_COOKIE); + // } @Transactional Entitlement ensureEntitlement(String name, String description) { @@ -519,7 +515,7 @@ void assertTicketCreateMail(Mail mail, Ticket ticket, String actorName, String s Assertions.assertTrue(mail.getHtml().contains(body)); } - Long createCompany(String cookie, String name) { + Long createCompany(String name) { String normalized = name == null ? "company" : name.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("(^-|-$)", ""); if (normalized.isBlank()) { @@ -527,11 +523,10 @@ Long createCompany(String cookie, String name) { } String superuserUsername = normalized + "-superuser"; String superuserEmail = normalized + "-superuser@testing.com"; - ensureUser(superuserUsername, superuserEmail, User.TYPE_SUPERUSER, "pass"); + ensureUser(superuserUsername, superuserEmail, User.TYPE_SUPERUSER); User superuser = User.find("email", superuserEmail).firstResult(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", name).formParam("userIds", superuser.id) - .post("/companies").then().statusCode(303); + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("name", name) + .formParam("userIds", superuser.id).post("/companies").then().statusCode(303); ai.mnemosyne_systems.model.Company company = ai.mnemosyne_systems.model.Company.find("name", name) .firstResult(); return company == null ? null : company.id; @@ -547,9 +542,9 @@ void setInstallationUse24HourClock(boolean use24HourClock) { } @Transactional - void deleteCompany(String cookie, Long companyId) { - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .post("/companies/" + companyId + "/delete").then().statusCode(303); + void deleteCompany(Long companyId) { + RestAssured.given().redirects().follow(false).post("/companies/" + companyId + "/delete").then() + .statusCode(303); } @Transactional diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/AdminAccessTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/AdminAccessTest.java index fcae78dd..a02a3c30 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/AdminAccessTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/AdminAccessTest.java @@ -46,156 +46,154 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class AdminAccessTest extends AccessTestSupport { @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void adminCanAccessAdminUsers() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - ensureUser("floating-user", "floating-user@mnemosyne-systems.ai", User.TYPE_USER, "password"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + ensureUser("floating-user", "floating-user@mnemosyne-systems.ai", User.TYPE_USER); ensureCompanyIfMissing("Test Co"); - String cookie = login("admin", "admin"); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/users").then() - .statusCode(303).header("Location", Matchers.endsWith("/users")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/admin/users").then().statusCode(200) - .body("title", Matchers.equalTo("Users")).body("showCompanySelector", Matchers.equalTo(true)) - .body("selectedCompanyId", Matchers.notNullValue()).body("items", Matchers.not(Matchers.empty())); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("companyId", 0).get("/api/admin/users") - .then().statusCode(200).body("selectedCompanyId", Matchers.equalTo(0)) - .body("companies.name", Matchers.hasItem("Unassigned")) + + RestAssured.given().redirects().follow(false).get("/users").then().statusCode(303).header("Location", + Matchers.endsWith("/users")); + RestAssured.given().get("/api/admin/users").then().statusCode(200).body("title", Matchers.equalTo("Users")) + .body("showCompanySelector", Matchers.equalTo(true)).body("selectedCompanyId", Matchers.notNullValue()) + .body("items", Matchers.not(Matchers.empty())); + RestAssured.given().queryParam("companyId", 0).get("/api/admin/users").then().statusCode(200) + .body("selectedCompanyId", Matchers.equalTo(0)).body("companies.name", Matchers.hasItem("Unassigned")) .body("items.email", Matchers.hasItem("floating-user@mnemosyne-systems.ai")); ai.mnemosyne_systems.model.User adminUser = ai.mnemosyne_systems.model.User .find("email", "admin@mnemosyne-systems.ai").firstResult(); Long adminId = adminUser == null ? null : adminUser.id; - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/user/" + adminId) - .then().statusCode(303).header("Location", Matchers.endsWith("/users/" + adminId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/admin/users/" + adminId).then() - .statusCode(200).body("username", Matchers.equalTo("admin")) + RestAssured.given().redirects().follow(false).get("/user/" + adminId).then().statusCode(303).header("Location", + Matchers.endsWith("/users/" + adminId)); + RestAssured.given().get("/api/admin/users/" + adminId).then().statusCode(200) + .body("username", Matchers.equalTo("admin")) .body("editPath", Matchers.equalTo("/users/" + adminId + "/edit")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/admin").then() - .statusCode(303).header("Location", Matchers.endsWith("/")); + RestAssured.given().redirects().follow(false).get("/admin").then().statusCode(303).header("Location", + Matchers.endsWith("/")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/companies").then() - .statusCode(303).header("Location", Matchers.endsWith("/companies")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/owner").then() - .statusCode(303).header("Location", Matchers.endsWith("/owner")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/owner").then().statusCode(200) - .body("name", Matchers.equalTo("mnemosyne systems")).body("use24HourClock", Matchers.equalTo(false)) - .body("supportOptions", Matchers.not(Matchers.empty())) + RestAssured.given().redirects().follow(false).get("/companies").then().statusCode(303).header("Location", + Matchers.endsWith("/companies")); + RestAssured.given().redirects().follow(false).get("/owner").then().statusCode(303).header("Location", + Matchers.endsWith("/owner")); + RestAssured.given().get("/api/owner").then().statusCode(200).body("name", Matchers.equalTo("mnemosyne systems")) + .body("use24HourClock", Matchers.equalTo(false)).body("supportOptions", Matchers.not(Matchers.empty())) .body("tamOptions", Matchers.not(Matchers.empty())); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/owner/edit").then() - .statusCode(303).header("Location", Matchers.endsWith("/owner/edit")); + RestAssured.given().redirects().follow(false).get("/owner/edit").then().statusCode(303).header("Location", + Matchers.endsWith("/owner/edit")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/users/create").then() - .statusCode(303).header("Location", Matchers.endsWith("/users/new")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/admin/users/bootstrap").then() - .statusCode(200).body("title", Matchers.equalTo("New user")) - .body("passwordRequired", Matchers.equalTo(true)).body("user.type", Matchers.equalTo("user")) - .body("user.name", Matchers.equalTo("")).body("user.email", Matchers.equalTo("")); + RestAssured.given().redirects().follow(false).get("/users/create").then().statusCode(303).header("Location", + Matchers.endsWith("/users/new")); + RestAssured.given().get("/api/admin/users/bootstrap").then().statusCode(200) + .body("title", Matchers.equalTo("New user")).body("passwordRequired", Matchers.equalTo(true)) + .body("user.type", Matchers.equalTo("user")).body("user.name", Matchers.equalTo("")) + .body("user.email", Matchers.equalTo("")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/entitlements").then() - .statusCode(303).header("Location", Matchers.endsWith("/entitlements")); + RestAssured.given().redirects().follow(false).get("/entitlements").then().statusCode(303).header("Location", + Matchers.endsWith("/entitlements")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/levels").then() - .statusCode(303).header("Location", Matchers.endsWith("/levels")); + RestAssured.given().redirects().follow(false).get("/levels").then().statusCode(303).header("Location", + Matchers.endsWith("/levels")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/companies/create") - .then().statusCode(303).header("Location", Matchers.endsWith("/companies/new")); + RestAssured.given().redirects().follow(false).get("/companies/create").then().statusCode(303).header("Location", + Matchers.endsWith("/companies/new")); - Long companyId = createCompany(cookie, "Cycle Co"); - deleteCompany(cookie, companyId); + Long companyId = createCompany("Cycle Co"); + deleteCompany(companyId); } @Test + @TestSecurity(user = "admin2@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin2@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void adminCanManageSupportCatalogAndCompanies() { - ensureUser("admin2", "admin2@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin2"); - String cookie = login("admin2", "admin2"); + ensureUser("admin2", "admin2@mnemosyne-systems.ai", User.TYPE_ADMIN); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/entitlements").then() - .statusCode(303).header("Location", Matchers.endsWith("/entitlements")); + RestAssured.given().redirects().follow(false).get("/entitlements").then().statusCode(303).header("Location", + Matchers.endsWith("/entitlements")); String entitlementName = "Test Entitlement"; - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", entitlementName) + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("name", entitlementName) .formParam("description", "Test description").formParam("versionNames", "1.0.0") .formParam("versionDates", "2026-01-01").post("/entitlements").then().statusCode(303); Entitlement entitlement = Entitlement.find("name", entitlementName).firstResult(); Assertions.assertNotNull(entitlement); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", "Updated Entitlement") - .formParam("description", "Updated description").formParam("versionNames", "1.0.1") - .formParam("versionDates", "2026-02-01").post("/entitlements/" + entitlement.id).then().statusCode(303); + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC) + .formParam("name", "Updated Entitlement").formParam("description", "Updated description") + .formParam("versionNames", "1.0.1").formParam("versionDates", "2026-02-01") + .post("/entitlements/" + entitlement.id).then().statusCode(303); Entitlement updatedEntitlement = refreshedEntitlement(entitlement.id); Assertions.assertEquals("Updated Entitlement", updatedEntitlement.name); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/entitlements/" + entitlement.id).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/entitlements/" + entitlement.id).then().statusCode(303) .header("Location", Matchers.endsWith("/entitlements/" + entitlement.id)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/entitlements/" + entitlement.id + "/edit").then().statusCode(303) - .header("Location", Matchers.endsWith("/entitlements/" + entitlement.id + "/edit")); + RestAssured.given().redirects().follow(false).get("/entitlements/" + entitlement.id + "/edit").then() + .statusCode(303).header("Location", Matchers.endsWith("/entitlements/" + entitlement.id + "/edit")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .post("/entitlements/" + entitlement.id + "/delete").then().statusCode(303); + RestAssured.given().redirects().follow(false).post("/entitlements/" + entitlement.id + "/delete").then() + .statusCode(303); Assertions.assertNull(refreshedEntitlement(entitlement.id)); String levelName = "Test Level"; - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", levelName) + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("name", levelName) .formParam("description", "Level description").formParam("level", 30).formParam("color", "Red") .post("/levels").then().statusCode(303); Level level = Level.find("name", levelName).firstResult(); Assertions.assertNotNull(level); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", "Updated Level") + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("name", "Updated Level") .formParam("description", "Updated level").formParam("level", 45).formParam("color", "Yellow") .post("/levels/" + level.id).then().statusCode(303); Level updatedLevel = refreshedLevel(level.id); Assertions.assertEquals("Updated Level", updatedLevel.name); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/levels").then() - .statusCode(303).header("Location", Matchers.endsWith("/levels")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/levels/" + level.id + "/edit").then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/levels").then().statusCode(303).header("Location", + Matchers.endsWith("/levels")); + RestAssured.given().redirects().follow(false).get("/levels/" + level.id + "/edit").then().statusCode(303) .header("Location", Matchers.endsWith("/levels/" + level.id + "/edit")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .post("/levels/" + level.id + "/delete").then().statusCode(303); + RestAssured.given().redirects().follow(false).post("/levels/" + level.id + "/delete").then().statusCode(303); Assertions.assertNull(refreshedLevel(level.id)); - ensureUser("coverage-superuser", "coverage-superuser@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "password"); + ensureUser("coverage-superuser", "coverage-superuser@mnemosyne-systems.ai", User.TYPE_SUPERUSER); User coverageSuperuser = User.find("email", "coverage-superuser@mnemosyne-systems.ai").firstResult(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", "Coverage Co") + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("name", "Coverage Co") .formParam("userIds", coverageSuperuser.id).post("/companies").then().statusCode(303); Company company = Company.find("name", "Coverage Co").firstResult(); Assertions.assertNotNull(company); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", "Coverage Co Updated") - .formParam("country", "United States of America").post("/companies/" + company.id).then() - .statusCode(303); + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC) + .formParam("name", "Coverage Co Updated").formParam("country", "United States of America") + .post("/companies/" + company.id).then().statusCode(303); Company updatedCompany = refreshedCompany(company.id); Assertions.assertEquals("Coverage Co Updated", updatedCompany.name); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .post("/companies/" + company.id + "/delete").then().statusCode(303); + RestAssured.given().redirects().follow(false).post("/companies/" + company.id + "/delete").then() + .statusCode(303); Assertions.assertNull(refreshedCompany(company.id)); } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactAppSessionReturnsAdminCategoryLink() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - String cookie = login("admin", "admin"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/app/session").then().statusCode(200) - .body("role", Matchers.equalTo("admin")).body("navigation.href", Matchers.hasItem("/categories")) + RestAssured.given().get("/api/app/session").then().statusCode(200).body("role", Matchers.equalTo("admin")) + .body("navigation.href", Matchers.hasItem("/categories")) .body("installationLogoBase64", Matchers.startsWith("data:image/svg+xml;base64,")) .body("installationBackgroundBase64", Matchers.anyOf(Matchers.nullValue(), Matchers.startsWith("data:image/"))) @@ -208,12 +206,14 @@ void reactAppSessionReturnsAdminCategoryLink() { } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactAppSessionReturnsAdminOwnerLink() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - String cookie = login("admin", "admin"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/app/session").then().statusCode(200) - .body("role", Matchers.equalTo("admin")).body("navigation.href", Matchers.hasItem("/owner")) + RestAssured.given().get("/api/app/session").then().statusCode(200).body("role", Matchers.equalTo("admin")) + .body("navigation.href", Matchers.hasItem("/owner")) .body("installationLogoBase64", Matchers.startsWith("data:image/svg+xml;base64,")) .body("installationBackgroundBase64", Matchers.anyOf(Matchers.nullValue(), Matchers.startsWith("data:image/"))) @@ -226,12 +226,14 @@ void reactAppSessionReturnsAdminOwnerLink() { } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactAppSessionReturnsAdminCompanyLink() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - String cookie = login("admin", "admin"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/app/session").then().statusCode(200) - .body("role", Matchers.equalTo("admin")).body("navigation.href", Matchers.hasItem("/companies")) + RestAssured.given().get("/api/app/session").then().statusCode(200).body("role", Matchers.equalTo("admin")) + .body("navigation.href", Matchers.hasItem("/companies")) .body("installationLogoBase64", Matchers.startsWith("data:image/svg+xml;base64,")) .body("installationBackgroundBase64", Matchers.anyOf(Matchers.nullValue(), Matchers.startsWith("data:image/"))) @@ -243,38 +245,42 @@ void reactAppSessionReturnsAdminCompanyLink() { .body("inactivityWarningSeconds", Matchers.equalTo(AuthHelper.WARNING_LEAD_SECONDS)); } + // @Test + // void reactLoginRedirectsAdminToAdminDashboard() { + // ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + // + // RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("username", "admin") + // .formParam("password", "admin").post("/login").then().statusCode(303) + // .header("Location", Matchers.equalTo("/")); + // } @Test - void reactLoginRedirectsAdminToAdminDashboard() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - - RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("username", "admin") - .formParam("password", "admin").post("/login").then().statusCode(303) - .header("Location", Matchers.equalTo("/")); - } - - @Test + @TestSecurity(user = "support1@mnemosyne-systems.ai", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactCategoriesApiRequiresAdminPrivileges() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - String cookie = login("support1", "support1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/categories").then().statusCode(401); + RestAssured.given().get("/api/categories").then().statusCode(403); } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactCategoriesApiReturnsAdminCategoryListAndDetail() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - String cookie = login("admin", "admin"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + Category category = ensureCategory("Escalation", "First line\n\nExtra detail", true); Attachment attachment = ensureCategoryAttachment(category, "category-note.txt", "Category attachment"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/categories").then().statusCode(200) - .body("canCreate", Matchers.equalTo(true)).body("createPath", Matchers.equalTo("/categories/new")) + RestAssured.given().get("/api/categories").then().statusCode(200).body("canCreate", Matchers.equalTo(true)) + .body("createPath", Matchers.equalTo("/categories/new")) .body("items.name", Matchers.hasItem("Escalation")) .body("items.find { it.name == 'Escalation' }.descriptionPreview", Matchers.equalTo("First line")) .body("items.find { it.name == 'Escalation' }.isDefault", Matchers.equalTo(true)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/categories/" + category.id).then() - .statusCode(200).body("name", Matchers.equalTo("Escalation")) + RestAssured.given().get("/api/categories/" + category.id).then().statusCode(200) + .body("name", Matchers.equalTo("Escalation")) .body("description", Matchers.containsString("Extra detail")).body("isDefault", Matchers.equalTo(true)) .body("editPath", Matchers.equalTo("/categories/" + category.id + "/edit")) .body("attachments.name", Matchers.hasItem("category-note.txt")) @@ -282,42 +288,49 @@ void reactCategoriesApiReturnsAdminCategoryListAndDetail() { } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactArticlesApiReturnsAdminArticleListAndDetail() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - String cookie = login("admin", "admin"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + Article article = ensureArticle("Admin Runbook", "ops,admin", "## Admin article\n\nSteps"); Attachment attachment = ensureArticleAttachment(article, "admin-runbook.txt", "Admin attachment"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/articles").then().statusCode(200) - .body("canCreate", Matchers.equalTo(true)).body("createPath", Matchers.equalTo("/articles/new")) + RestAssured.given().get("/api/articles").then().statusCode(200).body("canCreate", Matchers.equalTo(true)) + .body("createPath", Matchers.equalTo("/articles/new")) .body("items.title", Matchers.hasItem("Admin Runbook")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/articles/" + article.id).then() - .statusCode(200).body("title", Matchers.equalTo("Admin Runbook")) - .body("tags", Matchers.equalTo("ops,admin")).body("body", Matchers.containsString("Admin article")) - .body("canEdit", Matchers.equalTo(true)).body("canDelete", Matchers.equalTo(true)) + RestAssured.given().get("/api/articles/" + article.id).then().statusCode(200) + .body("title", Matchers.equalTo("Admin Runbook")).body("tags", Matchers.equalTo("ops,admin")) + .body("body", Matchers.containsString("Admin article")).body("canEdit", Matchers.equalTo(true)) + .body("canDelete", Matchers.equalTo(true)) .body("editPath", Matchers.equalTo("/articles/" + article.id + "/edit")) .body("attachments.name", Matchers.hasItem("admin-runbook.txt")) .body("attachments.downloadPath", Matchers.hasItem("/attachments/" + attachment.id + "/data")); } @Test + @TestSecurity(user = "support1@mnemosyne-systems.ai", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactCompaniesApiRequiresAdminPrivileges() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - String cookie = login("support1", "support1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/companies").then().statusCode(401); + RestAssured.given().get("/api/companies").then().statusCode(403); } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactCompaniesApiReturnsAdminCompanyListAndDetail() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("assigned-user", "assigned-user@mnemosyne-systems.ai", User.TYPE_USER, "assigned-user"); - ensureUser("assigned-superuser", "assigned-superuser@mnemosyne-systems.ai", User.TYPE_SUPERUSER, - "assigned-superuser"); - String cookie = login("admin", "admin"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("assigned-user", "assigned-user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("assigned-superuser", "assigned-superuser@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + Long companyId = ensureCompany("React Company"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Long assignedCompanyId = ensureCompany("Assigned Company"); @@ -328,14 +341,14 @@ void reactCompaniesApiReturnsAdminCompanyListAndDetail() { Level level = ensureLevel("React Level", "React level detail", 60, "Blue"); ensureCompanyEntitlement(company, entitlement, level); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/companies").then().statusCode(200) + RestAssured.given().get("/api/companies").then().statusCode(200) .body("createPath", Matchers.equalTo("/companies/new")) .body("items.name", Matchers.hasItem("React Company")) .body("items.find { it.name == 'React Company' }.detailPath", Matchers.equalTo("/companies/" + companyId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/companies/" + companyId).then() - .statusCode(200).body("name", Matchers.equalTo("React Company")) + RestAssured.given().get("/api/companies/" + companyId).then().statusCode(200) + .body("name", Matchers.equalTo("React Company")) .body("selectedUsers.email", Matchers.hasItem("user@mnemosyne-systems.ai")) .body("selectedTams.email", Matchers.hasItem("tam1@mnemosyne-systems.ai")) .body("userOptions.email", Matchers.not(Matchers.hasItem("assigned-user@mnemosyne-systems.ai"))) @@ -346,18 +359,20 @@ void reactCompaniesApiReturnsAdminCompanyListAndDetail() { } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void adminCanUpdateCompanyWithEntitlementAssignments() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - String cookie = login("admin", "admin"); - Long companyId = createCompany(cookie, "Entitlement Update Co"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + + Long companyId = createCompany("Entitlement Update Co"); Company company = refreshedCompany(companyId); Assertions.assertNotNull(company); Entitlement entitlement = ensureEntitlement("Company Update Entitlement", "Update detail"); Level level = ensureLevel("Company Update Level", "Update level", 50, "Orange"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("name", company.name) + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("name", company.name) .formParam("entitlementIds", entitlement.id).formParam("levelIds", level.id) .formParam("entitlementDates", "2026-03-27") .formParam("entitlementDurations", CompanyEntitlement.DURATION_YEARLY).post("/companies/" + company.id) @@ -373,25 +388,29 @@ void adminCanUpdateCompanyWithEntitlementAssignments() { Company updatedCompany = refreshedCompany(company.id); Assertions.assertTrue(companyHasUser(updatedCompany.id, "entitlement-update-co-superuser@testing.com")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/companies/" + company.id).then() - .statusCode(200) - .body("selectedSuperusers.email", Matchers.hasItem("entitlement-update-co-superuser@testing.com")); + RestAssured.given().get("/api/companies/" + company.id).then().statusCode(200).body("selectedSuperusers.email", + Matchers.hasItem("entitlement-update-co-superuser@testing.com")); } @Test + @TestSecurity(user = "support1@mnemosyne-systems.ai", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactOwnerApiRequiresAdminPrivileges() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - String cookie = login("support1", "support1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/owner").then().statusCode(401); + RestAssured.given().get("/api/owner").then().statusCode(403); } @Test + @TestSecurity(user = "admin@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) void reactOwnerApiReturnsAndUpdatesOwnerDetails() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - String cookie = login("admin", "admin"); + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + Company ownerCompany = ownerCompany(); Country country = Country.find("code", "US").firstResult(); Timezone timezone = Timezone.find("country = ?1 order by name", country).firstResult(); @@ -400,7 +419,7 @@ void reactOwnerApiReturnsAndUpdatesOwnerDetails() { String installationLogo = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNCIgZmlsbD0iIzEyMzQ1NiIvPjwvc3ZnPg=="; String installationBackground = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jx6kAAAAASUVORK5CYII="; - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/owner").then().statusCode(200) + RestAssured.given().get("/api/owner").then().statusCode(200) .body("id", Matchers.equalTo(ownerCompany.id.intValue())) .body("name", Matchers.equalTo(ownerCompany.name)).body("countries.size()", Matchers.greaterThan(0)) .body("timezones.size()", Matchers.greaterThan(0)) @@ -412,7 +431,7 @@ void reactOwnerApiReturnsAndUpdatesOwnerDetails() { .body("supportOptions.email", Matchers.hasItem("support1@mnemosyne-systems.ai")) .body("tamOptions.email", Matchers.hasItem("tam1@mnemosyne-systems.ai")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType(ContentType.JSON) + RestAssured.given().contentType(ContentType.JSON) .body(Map.ofEntries(Map.entry("name", "mnemosyne systems"), Map.entry("address1", "Owner Street 1"), Map.entry("address2", "Owner Street 2"), Map.entry("city", "Owner City"), Map.entry("state", "Owner State"), Map.entry("zip", "12345"), @@ -445,7 +464,7 @@ void reactOwnerApiReturnsAndUpdatesOwnerDetails() { Assertions.assertEquals(installationBackground, installation.backgroundBase64); Assertions.assertTrue(companyHasUser(ownerCompany.id, "support1@mnemosyne-systems.ai")); Assertions.assertTrue(companyHasUser(ownerCompany.id, "tam1@mnemosyne-systems.ai")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/app/session").then().statusCode(200) + RestAssured.given().get("/api/app/session").then().statusCode(200) .body("installationLogoBase64", Matchers.equalTo(installationLogo)) .body("installationBackgroundBase64", Matchers.equalTo(installationBackground)) .body("installationHeaderFooterColor", Matchers.equalTo("#123456")) @@ -455,25 +474,26 @@ void reactOwnerApiReturnsAndUpdatesOwnerDetails() { } @Test + @TestSecurity(user = "admin-json@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin-json@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin-json") }) void reactAdminMutationsReturnJsonRedirects() { - ensureUser("admin-json", "admin-json@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin-json"); - String cookie = login("admin-json", "admin-json"); + ensureUser("admin-json", "admin-json@mnemosyne-systems.ai", User.TYPE_ADMIN); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("name", "react-admin-user") - .formParam("email", "react-admin-user@mnemosyne-systems.ai").formParam("type", User.TYPE_USER) - .formParam("password", "secret").post("/users").then().statusCode(200) + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC) + .formParam("name", "react-admin-user").formParam("email", "react-admin-user@mnemosyne-systems.ai") + .formParam("type", User.TYPE_USER).formParam("password", "secret").post("/users").then().statusCode(200) .body("redirectTo", Matchers.equalTo("/users")); User createdUser = User.find("email", "react-admin-user@mnemosyne-systems.ai").firstResult(); Assertions.assertNotNull(createdUser); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("name", "react-admin-user") + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC) + .formParam("name", "react-admin-user") .formParam("email", "react-admin-user-duplicate@mnemosyne-systems.ai").formParam("type", User.TYPE_USER) .formParam("password", "secret").post("/users").then().statusCode(400); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("name", "renamed-admin-user") + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC) + .formParam("name", "renamed-admin-user") .formParam("email", "react-admin-user-updated@mnemosyne-systems.ai") .formParam("type", User.TYPE_SUPPORT).post("/user/" + createdUser.id).then().statusCode(200) .body("redirectTo", Matchers.equalTo("/users")); @@ -482,28 +502,27 @@ void reactAdminMutationsReturnJsonRedirects() { Assertions.assertEquals("react-admin-user-updated@mnemosyne-systems.ai", refreshedCreatedUser.email); Assertions.assertEquals(User.TYPE_SUPPORT, refreshedCreatedUser.type); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .multiPart("name", "React Redirect Category").multiPart("description", "Redirect body") - .multiPart("isDefault", "false").post("/categories").then().statusCode(200) - .body("redirectTo", Matchers.equalTo("/categories")); + RestAssured.given().header("X-Billetsys-Client", "react").multiPart("name", "React Redirect Category") + .multiPart("description", "Redirect body").multiPart("isDefault", "false").post("/categories").then() + .statusCode(200).body("redirectTo", Matchers.equalTo("/categories")); Category category = Category.find("name", "React Redirect Category").firstResult(); Assertions.assertNotNull(category); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .post("/categories/" + category.id + "/delete").then().statusCode(200) - .body("redirectTo", Matchers.equalTo("/categories")); + RestAssured.given().header("X-Billetsys-Client", "react").post("/categories/" + category.id + "/delete").then() + .statusCode(200).body("redirectTo", Matchers.equalTo("/categories")); } @Test + @TestSecurity(user = "admin-delete@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin-delete@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin-delete") }) void adminCanDeleteReferencedUserAssignedToCompany() { - ensureUser("admin-delete", "admin-delete@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin-delete"); - ensureUser("delete-target", "delete-target@mnemosyne-systems.ai", User.TYPE_USER, "delete-target"); - String cookie = login("admin-delete", "admin-delete"); + ensureUser("admin-delete", "admin-delete@mnemosyne-systems.ai", User.TYPE_ADMIN); + ensureUser("delete-target", "delete-target@mnemosyne-systems.ai", User.TYPE_USER); Long companyId = ensureCompany("Delete User Co"); Long targetUserId = prepareReferencedUserForDelete(companyId, "delete-target@mnemosyne-systems.ai", true); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .post("/user/" + targetUserId + "/delete").then().statusCode(303) + RestAssured.given().redirects().follow(false).post("/user/" + targetUserId + "/delete").then().statusCode(303) .header("Location", Matchers.endsWith("/users")); Assertions.assertNull(refreshedUser(targetUserId)); @@ -513,15 +532,16 @@ void adminCanDeleteReferencedUserAssignedToCompany() { } @Test + @TestSecurity(user = "admin-delete-2@mnemosyne-systems.ai", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin-delete-2@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin-delete-2") }) void adminCanDeleteReferencedNonSuperuserWithoutServerError() { - ensureUser("admin-delete-2", "admin-delete-2@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin-delete-2"); - ensureUser("delete-target-2", "delete-target-2@mnemosyne-systems.ai", User.TYPE_USER, "delete-target-2"); - String cookie = login("admin-delete-2", "admin-delete-2"); + ensureUser("admin-delete-2", "admin-delete-2@mnemosyne-systems.ai", User.TYPE_ADMIN); + ensureUser("delete-target-2", "delete-target-2@mnemosyne-systems.ai", User.TYPE_USER); Long companyId = ensureCompany("Delete User Co 2"); Long targetUserId = prepareReferencedUserForDelete(companyId, "delete-target-2@mnemosyne-systems.ai", false); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .post("/user/" + targetUserId + "/delete").then().statusCode(303) + RestAssured.given().redirects().follow(false).post("/user/" + targetUserId + "/delete").then().statusCode(303) .header("Location", Matchers.endsWith("/users")); Assertions.assertNull(refreshedUser(targetUserId)); diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/MessagePrivacyAccessTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/MessagePrivacyAccessTest.java index ed9607f2..336b6a05 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/MessagePrivacyAccessTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/MessagePrivacyAccessTest.java @@ -22,16 +22,23 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class MessagePrivacyAccessTest extends AccessTestSupport { @Test - void privateMessagesAreVisibleOnlyToMatchingGroups() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) + void privateMessagesAreVisibleToSupport() { + // [CHANGE] ensureUser calls — removed password argument (Keycloak owns passwords) + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); Long companyId = ensureCompany("Message Privacy Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", @@ -49,39 +56,115 @@ void privateMessagesAreVisibleOnlyToMatchingGroups() { Attachment internalAttachment = ensureAttachment(internalMessage, "internal-private.txt"); Attachment externalAttachment = ensureAttachment(externalMessage, "external-private.txt"); - String supportCookie = login("support1", "support1"); - String tamCookie = login("tam1", "tam1"); - String userCookie = login("user", "user"); - String superuserCookie = login("superuser1", "superuser1"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, supportCookie).get("/api/support/tickets/" + ticket.id) - .then().statusCode(200).body("messages.body", Matchers.hasItems(publicBody, internalBody)) + RestAssured.given().get("/api/support/tickets/" + ticket.id).then().statusCode(200) + .body("messages.body", Matchers.hasItems(publicBody, internalBody)) .body("messages.body", Matchers.not(Matchers.hasItem(externalBody))) .body("messages.find { it.body == '" + internalBody + "' }.isPublic", Matchers.equalTo(false)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, tamCookie).get("/api/user/tickets/" + ticket.id).then() - .statusCode(200).body("messages.body", Matchers.hasItems(publicBody, internalBody)) + RestAssured.given().get("/attachments/" + externalAttachment.id + "/data").then().statusCode(404); + } + + @Test + @TestSecurity(user = "tam1", roles = "tam") + @JwtSecurity(claims = { @Claim(key = "email", value = "tam1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "tam") }) + void privateMessagesAreVisibleToTam() { + // [CHANGE] ensureUser calls — removed password argument + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + + Long companyId = ensureCompany("Message Privacy Co TAM"); + ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", + "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); + String publicBody = "public-visible-tam-" + System.nanoTime(); + String internalBody = "internal-private-tam-" + System.nanoTime(); + String externalBody = "external-private-tam-" + System.nanoTime(); + ensureTimedMessage(ticket, publicBody, "support1@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(1), true); + ensureTimedMessage(ticket, internalBody, "support1@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(2), false); + ensureTimedMessage(ticket, externalBody, "user@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(3), false); + + RestAssured.given().get("/api/user/tickets/" + ticket.id).then().statusCode(200) + .body("messages.body", Matchers.hasItems(publicBody, internalBody)) .body("messages.body", Matchers.not(Matchers.hasItem(externalBody))); + } - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/api/user/tickets/" + ticket.id).then() - .statusCode(200).body("messages.body", Matchers.hasItems(publicBody, externalBody)) + @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void privateMessagesAreVisibleToUser() { + // [CHANGE] ensureUser calls — removed password argument + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + + Long companyId = ensureCompany("Message Privacy Co User"); + ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", + "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); + String publicBody = "public-visible-user-" + System.nanoTime(); + String internalBody = "internal-private-user-" + System.nanoTime(); + String externalBody = "external-private-user-" + System.nanoTime(); + ensureTimedMessage(ticket, publicBody, "support1@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(1), true); + Message internalMessage = ensureTimedMessage(ticket, internalBody, "support1@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(2), false); + ensureTimedMessage(ticket, externalBody, "user@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(3), false); + Attachment internalAttachment = ensureAttachment(internalMessage, "internal-private.txt"); + + RestAssured.given().get("/api/user/tickets/" + ticket.id).then().statusCode(200) + .body("messages.body", Matchers.hasItems(publicBody, externalBody)) .body("messages.body", Matchers.not(Matchers.hasItem(internalBody))) .body("messages.find { it.body == '" + externalBody + "' }.isPublic", Matchers.equalTo(false)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, superuserCookie).get("/api/superuser/tickets/" + ticket.id) - .then().statusCode(200).body("messages.body", Matchers.hasItems(publicBody, externalBody)) - .body("messages.body", Matchers.not(Matchers.hasItem(internalBody))); + RestAssured.given().get("/api/attachments/" + internalAttachment.id).then().statusCode(404); + } - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/api/attachments/" + internalAttachment.id) - .then().statusCode(404); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .get("/attachments/" + externalAttachment.id + "/data").then().statusCode(404); + @Test + @TestSecurity(user = "superuser1", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser") }) + void privateMessagesAreVisibleToSuperuser() { + // [CHANGE] ensureUser calls — removed password argument + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + + Long companyId = ensureCompany("Message Privacy Co SU"); + ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", + "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); + String publicBody = "public-visible-su-" + System.nanoTime(); + String internalBody = "internal-private-su-" + System.nanoTime(); + String externalBody = "external-private-su-" + System.nanoTime(); + ensureTimedMessage(ticket, publicBody, "support1@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(1), true); + ensureTimedMessage(ticket, internalBody, "support1@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(2), false); + ensureTimedMessage(ticket, externalBody, "user@mnemosyne-systems.ai", + java.time.LocalDateTime.now().minusMinutes(3), false); + + RestAssured.given().get("/api/superuser/tickets/" + ticket.id).then().statusCode(200) + .body("messages.body", Matchers.hasItems(publicBody, externalBody)) + .body("messages.body", Matchers.not(Matchers.hasItem(internalBody))); } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void privateVisibilityFlagPersistsForCreatedAndReplyMessages() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Private Create Co"); @@ -94,64 +177,79 @@ void privateVisibilityFlagPersistsForCreatedAndReplyMessages() { .firstResult(); Assertions.assertNotNull(affectsVersion); - String supportCookie = login("support1", "support1"); String privateCreateBody = "private-create-message-" + System.nanoTime(); String privateReplyBody = "private-reply-message-" + System.nanoTime(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .multiPart("status", "Open").multiPart("title", "Private create title") - .multiPart("message", privateCreateBody).multiPart("isPublic", "false") - .multiPart("companyId", companyId).multiPart("companyEntitlementId", entitlement.id) - .multiPart("categoryId", Category.findDefault().id).multiPart("affectsVersionId", affectsVersion.id) - .post("/support/tickets").then().statusCode(303); + RestAssured.given().redirects().follow(false).multiPart("status", "Open") + .multiPart("title", "Private create title").multiPart("message", privateCreateBody) + .multiPart("isPublic", "false").multiPart("companyId", companyId) + .multiPart("companyEntitlementId", entitlement.id).multiPart("categoryId", Category.findDefault().id) + .multiPart("affectsVersionId", affectsVersion.id).post("/support/tickets").then().statusCode(303); Message createdMessage = Message.find("body", privateCreateBody).firstResult(); Assertions.assertNotNull(createdMessage); Assertions.assertFalse(createdMessage.isPublic); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .multiPart("body", privateReplyBody).multiPart("isPublic", "false") + RestAssured.given().redirects().follow(false).multiPart("body", privateReplyBody).multiPart("isPublic", "false") .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303); Message replyMessage = Message.find("ticket = ?1 and body = ?2", ticket, privateReplyBody).firstResult(); Assertions.assertNotNull(replyMessage); Assertions.assertFalse(replyMessage.isPublic); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, supportCookie).get("/api/support/tickets/" + ticket.id) - .then().statusCode(200) + RestAssured.given().get("/api/support/tickets/" + ticket.id).then().statusCode(200) .body("messages.find { it.body == '" + privateReplyBody + "' }.isPublic", Matchers.equalTo(false)); } @Test - void privateMessageEmailsStayWithinTheirAudience() { + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) + void privateMessageEmailsFromSupportStayWithinTheirAudience() { mailbox.clear(); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); + // [CHANGE] ensureUser calls — removed password argument + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); Long companyId = ensureCompany("Private Mail Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureTicket(companyId); - String supportCookie = login("support1", "support1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .multiPart("body", "Private support email " + System.nanoTime()).multiPart("isPublic", "false") - .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303); + RestAssured.given().redirects().follow(false).multiPart("body", "Private support email " + System.nanoTime()) + .multiPart("isPublic", "false").post("/support/tickets/" + ticket.id + "/messages").then() + .statusCode(303); + // Internal message (support-authored): only support + tam should receive email Assertions.assertFalse(mailbox.getMailsSentTo("support1@mnemosyne-systems.ai").isEmpty()); Assertions.assertFalse(mailbox.getMailsSentTo("tam1@mnemosyne-systems.ai").isEmpty()); Assertions.assertTrue(mailbox.getMailsSentTo("user@mnemosyne-systems.ai").isEmpty()); Assertions.assertTrue(mailbox.getMailsSentTo("superuser1@mnemosyne-systems.ai").isEmpty()); + } + @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void privateMessageEmailsFromUserStayWithinTheirAudience() { mailbox.clear(); + // [CHANGE] ensureUser calls — removed password argument + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + + Long companyId = ensureCompany("Private Mail Co User"); + ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", + "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); - String userCookie = login("user", "user"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, userCookie) - .multiPart("body", "Private requester email " + System.nanoTime()).multiPart("isPublic", "false") - .post("/user/tickets/" + ticket.id + "/messages").then().statusCode(303); + RestAssured.given().redirects().follow(false).multiPart("body", "Private requester email " + System.nanoTime()) + .multiPart("isPublic", "false").post("/user/tickets/" + ticket.id + "/messages").then().statusCode(303); + // External message (user-authored): only user + superuser should receive email Assertions.assertFalse(mailbox.getMailsSentTo("user@mnemosyne-systems.ai").isEmpty()); Assertions.assertFalse(mailbox.getMailsSentTo("superuser1@mnemosyne-systems.ai").isEmpty()); Assertions.assertTrue(mailbox.getMailsSentTo("support1@mnemosyne-systems.ai").isEmpty()); @@ -159,13 +257,16 @@ void privateMessageEmailsStayWithinTheirAudience() { } @Test - void privateMessagesDoNotChangeOverviewSlaColors() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); - - Long companyId = ensureCompany("Private SLA Co"); + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void privateMessagesDoNotChangeUserViewSlaColor() { + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + + Long companyId = ensureCompany("Private SLA Co User"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Level urgent = ensureLevel("Normal", "Immediate response", 1, "Red"); @@ -175,31 +276,48 @@ void privateMessagesDoNotChangeOverviewSlaColors() { ensureTimedMessage(userViewTicket, "Old requester message", "user@mnemosyne-systems.ai", java.time.LocalDateTime.now().minusMinutes(10), true); - String userCookie = login("user", "user"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/api/user/tickets").then().statusCode(200) + // SLA color starts as Red (pending requester reply) + RestAssured.given().get("/api/user/tickets").then().statusCode(200) .body("items.find { it.id == " + userViewTicket.id + " }.slaColor", Matchers.equalTo("Red")); + // A private support message must NOT change the SLA color visible to the user ensureTimedMessage(userViewTicket, "Private support update", "support1@mnemosyne-systems.ai", java.time.LocalDateTime.now(), false); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/api/user/tickets").then().statusCode(200) + RestAssured.given().get("/api/user/tickets").then().statusCode(200) .body("items.find { it.id == " + userViewTicket.id + " }.slaColor", Matchers.equalTo("Red")); + } + + @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) + void privateMessagesDoNotChangeSupportViewSlaColor() { + // [CHANGE] ensureUser calls — removed password argument + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + + Long companyId = ensureCompany("Private SLA Co Support"); + ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", + "tam1@mnemosyne-systems.ai"); + Level urgent = ensureLevel("Normal", "Immediate response", 1, "Red"); Ticket supportViewTicket = ensureTicket(companyId); setTicketSupportLevel(supportViewTicket.id, urgent.id); ensureTimedMessage(supportViewTicket, "Old support message", "support1@mnemosyne-systems.ai", java.time.LocalDateTime.now().minusMinutes(10), true); - String supportCookie = login("support1", "support1"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, supportCookie).get("/api/support/tickets").then() - .statusCode(200) + // SLA color starts as Red (pending support reply) + RestAssured.given().get("/api/support/tickets").then().statusCode(200) .body("items.find { it.id == " + supportViewTicket.id + " }.slaColor", Matchers.equalTo("Red")); + // A private user message must NOT change the SLA color visible to support ensureTimedMessage(supportViewTicket, "Private user update", "user@mnemosyne-systems.ai", java.time.LocalDateTime.now(), false); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, supportCookie).get("/api/support/tickets").then() - .statusCode(200) + RestAssured.given().get("/api/support/tickets").then().statusCode(200) .body("items.find { it.id == " + supportViewTicket.id + " }.slaColor", Matchers.equalTo("Red")); } } diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/PasswordResetResourceTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/PasswordResetResourceTest.java index 69bb1513..0e381fe9 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/PasswordResetResourceTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/PasswordResetResourceTest.java @@ -10,7 +10,6 @@ import ai.mnemosyne_systems.model.PasswordResetToken; import ai.mnemosyne_systems.model.User; -import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.mailer.Mail; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTestProfile; @@ -56,8 +55,6 @@ void resetPasswordUpdatesPasswordAndDeletesToken() { Assertions.assertTrue(response.updated()); User refreshedUser = refreshedUser(findUserId("user2@mnemosyne-systems.ai")); - Assertions.assertTrue(BcryptUtil.matches("new-pass", refreshedUser.passwordHash)); - Assertions.assertFalse(BcryptUtil.matches("user2", refreshedUser.passwordHash)); Assertions.assertNull(findToken(token)); } diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/SuperuserAccessTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/SuperuserAccessTest.java index e8ccb526..15bd5070 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/SuperuserAccessTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/SuperuserAccessTest.java @@ -45,14 +45,20 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class SuperuserAccessTest extends AccessTestSupport { @Test + @TestSecurity(user = "superuser1", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser1") }) void superuserCanAccessCompanyScopedPages() { - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); Entitlement createPageEntitlement = ensureEntitlement("Superuser Create Versions", "Superuser create version list"); ensureVersion(createPageEntitlement, "7.7.7", java.time.LocalDate.of(2024, 7, 1)); @@ -62,18 +68,15 @@ void superuserCanAccessCompanyScopedPages() { ai.mnemosyne_systems.model.Ticket superuserTicket = ensureTicket(companyId); ensureMessage(superuserTicket, "Superuser ticket message"); ai.mnemosyne_systems.model.Ticket otherCompanyTicket = ensureTicket(otherCompanyId); - String cookie = login("superuser1", "superuser1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/").then() - .statusCode(303).header("Location", "/"); + RestAssured.given().redirects().follow(false).get("/").then().statusCode(303).header("Location", "/"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/superuser").then() - .statusCode(303).header("Location", Matchers.endsWith("/superuser/tickets")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/superuser/tickets/create").then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/superuser").then().statusCode(303).header("Location", + Matchers.endsWith("/superuser/tickets")); + RestAssured.given().redirects().follow(false).get("/superuser/tickets/create").then().statusCode(303) .header("Location", Matchers.endsWith("/superuser/tickets/new")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/superuser/tickets/bootstrap").then() - .statusCode(200).body("submitPath", Matchers.equalTo("/superuser/tickets")) + RestAssured.given().get("/api/superuser/tickets/bootstrap").then().statusCode(200) + .body("submitPath", Matchers.equalTo("/superuser/tickets")) .body("defaultAffectsVersion.name", Matchers.equalTo("1.0.0")); User creatingSuperuser = User.find("email", "superuser1@mnemosyne-systems.ai").firstResult(); CompanyEntitlement superuserCreateEntitlement = CompanyEntitlement @@ -84,8 +87,7 @@ void superuserCanAccessCompanyScopedPages() { .firstResult(); Assertions.assertNotNull(superuserCreateVersion); String superuserCreateMessage = "Superuser create redirect coverage"; - String superuserCreateRedirect = RestAssured.given().redirects().follow(false) - .cookie(AuthHelper.AUTH_COOKIE, cookie).multiPart("status", "Open") + String superuserCreateRedirect = RestAssured.given().redirects().follow(false).multiPart("status", "Open") .multiPart("title", "Superuser create redirect title").multiPart("message", superuserCreateMessage) .multiPart("companyId", companyId).multiPart("companyEntitlementId", superuserCreateEntitlement.id) .multiPart("categoryId", Category.findDefault().id) @@ -99,53 +101,52 @@ void superuserCanAccessCompanyScopedPages() { Assertions.assertEquals(superuserCreateVersion.id, createdSuperuserTicket.affectsVersion.id); Assertions.assertTrue(superuserCreateRedirect.endsWith("/superuser/tickets/" + createdSuperuserTicket.id)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/superuser/users/" + companyId).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/superuser/users/" + companyId).then().statusCode(303) .header("Location", Matchers.endsWith("/superuser/users?companyId=" + companyId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("companyId", companyId) - .get("/api/superuser/users").then().statusCode(200).body("title", Matchers.equalTo("Users")) + RestAssured.given().queryParam("companyId", companyId).get("/api/superuser/users").then().statusCode(200) + .body("title", Matchers.equalTo("Users")) .body("selectedCompanyId", Matchers.equalTo(companyId.intValue())) .body("items.username", Matchers.hasItems("superuser1", "tam1")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/reports/superuser") - .then().statusCode(303).header("Location", Matchers.endsWith("/reports")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/reports").then().statusCode(200) - .body("role", Matchers.equalTo("superuser")).body("showCompanyFilter", Matchers.equalTo(false)) - .body("selectedCompanyId", Matchers.notNullValue()).body("companyName", Matchers.not("All")); + RestAssured.given().redirects().follow(false).get("/reports/superuser").then().statusCode(303) + .header("Location", Matchers.endsWith("/reports")); + RestAssured.given().get("/api/reports").then().statusCode(200).body("role", Matchers.equalTo("superuser")) + .body("showCompanyFilter", Matchers.equalTo(false)).body("selectedCompanyId", Matchers.notNullValue()) + .body("companyName", Matchers.not("All")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/rss/superuser").then().statusCode(200) + RestAssured.given().get("/rss/superuser").then().statusCode(200) .contentType(Matchers.containsString("application/rss+xml")) .body(Matchers.containsString("Superuser tickets feed")); Long ticketId = superuserTicket == null ? null : superuserTicket.id; - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/superuser/tickets/" + ticketId).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/superuser/tickets/" + ticketId).then().statusCode(303) .header("Location", Matchers.endsWith("/superuser/tickets/" + ticketId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/superuser/tickets/" + ticketId).then() - .statusCode(200).body("title", Matchers.equalTo(superuserTicket.displayTitle())) + RestAssured.given().get("/api/superuser/tickets/" + ticketId).then().statusCode(200) + .body("title", Matchers.equalTo(superuserTicket.displayTitle())) .body("secondaryUsersLabel", Matchers.equalTo("Superusers")) .body("secondaryUsers.username", Matchers.hasItem("superuser1")) .body("supportUsers.username", Matchers.hasItem("support1")) .body("editableResolvedVersion", Matchers.equalTo(true)) .body("exportPath", Matchers.equalTo("/tickets/export/" + ticketId)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/tickets/" + ticketId) - .then().statusCode(303).header("Location", Matchers.endsWith("/superuser/tickets/" + ticketId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("pageSize", 100) - .get("/api/superuser/tickets").then().statusCode(200) + RestAssured.given().redirects().follow(false).get("/tickets/" + ticketId).then().statusCode(303) + .header("Location", Matchers.endsWith("/superuser/tickets/" + ticketId)); + RestAssured.given().queryParam("pageSize", 100).get("/api/superuser/tickets").then().statusCode(200) .body("items.name", Matchers.hasItem(superuserTicket.name)) .body("items.name", Matchers.not(Matchers.hasItem(otherCompanyTicket.name))); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/superuser/companies/" + otherCompanyId).then().statusCode(303) - .header("Location", Matchers.endsWith("/")); + RestAssured.given().redirects().follow(false).get("/superuser/companies/" + otherCompanyId).then() + .statusCode(303).header("Location", Matchers.endsWith("/")); } @Test + @TestSecurity(user = "superuser1", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser1") }) void superuserReplyUsesLatestPendingMessageForTicketSlaColor() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); ensureDefaultCategories(); Long companyId = ensureCompany("Superuser SLA Co"); ensureCompanyUsers(companyId, "superuser1@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); @@ -155,72 +156,71 @@ void superuserReplyUsesLatestPendingMessageForTicketSlaColor() { ensureTimedMessage(ticket, "Initial superuser message", "superuser1@mnemosyne-systems.ai", java.time.LocalDateTime.now().minusMinutes(10)); - String cookie = login("superuser1", "superuser1"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("pageSize", 100) - .get("/api/superuser/tickets").then().statusCode(200) + RestAssured.given().queryParam("pageSize", 100).get("/api/superuser/tickets").then().statusCode(200) .body("items.find { it.id == " + ticket.id + " }.slaColor", Matchers.equalTo("Red")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("body", "Follow-up from superuser").post("/superuser/tickets/" + ticket.id + "/messages") - .then().statusCode(303); + RestAssured.given().redirects().follow(false).multiPart("body", "Follow-up from superuser") + .post("/superuser/tickets/" + ticket.id + "/messages").then().statusCode(303); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("pageSize", 100) - .get("/api/superuser/tickets").then().statusCode(200) + RestAssured.given().queryParam("pageSize", 100).get("/api/superuser/tickets").then().statusCode(200) .body("items.find { it.id == " + ticket.id + " }.slaColor", Matchers.equalTo("White")); ensureTimedMessage(ticket, "Support follow-up", "support1@mnemosyne-systems.ai", java.time.LocalDateTime.now().minusMinutes(1)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("pageSize", 100) - .get("/api/superuser/tickets").then().statusCode(200) + RestAssured.given().queryParam("pageSize", 100).get("/api/superuser/tickets").then().statusCode(200) .body("items.find { it.id == " + ticket.id + " }.slaColor", Matchers.equalTo("White")); } @Test + @TestSecurity(user = "superuser1", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser1") }) void superuserTicketSearchMatchesVisibleMessagesOnly() { - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); Long companyId = ensureCompany("Superuser Search Co"); ensureCompanyUsers(companyId, "superuser1@mnemosyne-systems.ai"); Ticket visibleTicket = ensureTicket(companyId); String visibleBody = "superuser-visible-search-" + System.nanoTime(); ensureMessageWithBody(visibleTicket, visibleBody); - String cookie = login("superuser1", "superuser1"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("q", visibleBody) - .get("/api/superuser/tickets").then().statusCode(200).body("searchTerm", Matchers.equalTo(visibleBody)) + RestAssured.given().queryParam("q", visibleBody).get("/api/superuser/tickets").then().statusCode(200) + .body("searchTerm", Matchers.equalTo(visibleBody)) .body("items.name", Matchers.hasItem(visibleTicket.name)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("q", "superuser-search-no-match") - .get("/api/superuser/tickets").then().statusCode(200).body("items.size()", Matchers.equalTo(0)); + RestAssured.given().queryParam("q", "superuser-search-no-match").get("/api/superuser/tickets").then() + .statusCode(200).body("items.size()", Matchers.equalTo(0)); } @Test + @TestSecurity(user = "superuser1", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser1") }) void reactAppSessionReturnsSuperuserTicketLink() { - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); setInstallationUse24HourClock(false); - String cookie = login("superuser1", "superuser1"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/app/session").then().statusCode(200) - .body("role", Matchers.equalTo("superuser")) + RestAssured.given().get("/api/app/session").then().statusCode(200).body("role", Matchers.equalTo("superuser")) .body("navigation.href", Matchers.hasItem("/superuser/tickets")) .body("installationUse24HourClock", Matchers.equalTo(false)); } @Test + @TestSecurity(user = "superuser-json", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser-json@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser-json") }) void reactSuperuserMutationsReturnJsonRedirects() { - ensureUser("superuser-json", "superuser-json@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser-json"); + ensureUser("superuser-json", "superuser-json@mnemosyne-systems.ai", User.TYPE_SUPERUSER); ensureDefaultCategories(); Long companyId = ensureCompany("Superuser Json Co"); ensureCompanyUsers(companyId, "superuser-json@mnemosyne-systems.ai"); - String cookie = login("superuser-json", "superuser-json"); Entitlement entitlement = ensureEntitlement("Superuser Json Entitlement", "Superuser json detail"); Version version = ensureVersion(entitlement, "11.0.0", java.time.LocalDate.of(2026, 3, 1)); CompanyEntitlement entry = ensureCompanyEntitlement(companyId, entitlement); Assertions.assertNotNull(entry); Assertions.assertNotNull(version); - String redirectTo = RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie) - .header("X-Billetsys-Client", "react").multiPart("status", "Open") + String redirectTo = RestAssured.given().header("X-Billetsys-Client", "react").multiPart("status", "Open") .multiPart("title", "Superuser json redirect title") .multiPart("message", "Superuser json redirect create").multiPart("companyId", companyId) .multiPart("companyEntitlementId", entry.id).multiPart("categoryId", Category.findDefault().id) @@ -228,10 +228,10 @@ void reactSuperuserMutationsReturnJsonRedirects() { .body("redirectTo", Matchers.matchesPattern("/superuser/tickets/\\d+")).extract().path("redirectTo"); Long createdTicketId = Long.valueOf(redirectTo.substring(redirectTo.lastIndexOf('/') + 1)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("title", "Superuser json redirect title") - .formParam("affectsVersionId", version.id).post("/superuser/tickets/" + createdTicketId).then() - .statusCode(200).body("redirectTo", Matchers.equalTo("/superuser/tickets/" + createdTicketId)); + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC) + .formParam("title", "Superuser json redirect title").formParam("affectsVersionId", version.id) + .post("/superuser/tickets/" + createdTicketId).then().statusCode(200) + .body("redirectTo", Matchers.equalTo("/superuser/tickets/" + createdTicketId)); } } diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/SupportAccessTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/SupportAccessTest.java index 49c6d226..e94ad9f1 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/SupportAccessTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/SupportAccessTest.java @@ -23,8 +23,12 @@ import ai.mnemosyne_systems.model.Version; import ai.mnemosyne_systems.service.MailboxPollingService; import ai.mnemosyne_systems.service.TicketEmailService; +// [REMOVE] import ai.mnemosyne_systems.util.AuthHelper; -- AUTH_COOKIE no longer needed +// NOTE: AuthHelper.INACTIVITY_TIMEOUT_SECONDS and WARNING_LEAD_SECONDS are still +// referenced below as installation config constants (not auth); replace with +// their literal values or a shared constant if AuthHelper is fully removed. import ai.mnemosyne_systems.util.AuthHelper; -import io.quarkus.elytron.security.common.BcryptUtil; +// [REMOVE] import io.quarkus.elytron.security.common.BcryptUtil; -- Keycloak owns passwords import io.quarkus.hibernate.orm.panache.Panache; import io.quarkus.mailer.Mail; import io.quarkus.mailer.MockMailbox; @@ -46,33 +50,38 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +// [ADD] Keycloak JWT test security imports replacing cookie-based auth +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class SupportAccessTest extends AccessTestSupport { @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void supportCanAccessSupportUsersMenu() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("support2", "support2@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support2"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("support2", "support2@mnemosyne-systems.ai", User.TYPE_SUPPORT); ensureDefaultCategories(); Long companyId = ensureCompany("Support Co"); ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai"); ai.mnemosyne_systems.model.Ticket supportTicket = ensureTicket(companyId); ensureMessage(supportTicket, "Sample ticket created."); String supportTicketName = supportTicket == null ? "" : supportTicket.name; - String cookie = login("support1", "support1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/support/users/" + companyId).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/support/users/" + companyId).then().statusCode(303) .header("Location", Matchers.endsWith("/support/users?companyId=" + companyId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("companyId", companyId) - .get("/api/support/users").then().statusCode(200).body("title", Matchers.equalTo("Users")) + RestAssured.given().queryParam("companyId", companyId).get("/api/support/users").then().statusCode(200) + .body("title", Matchers.equalTo("Users")) .body("selectedCompanyId", Matchers.equalTo(companyId.intValue())) .body("createPath", Matchers.equalTo("/support/users/new?companyId=" + companyId)) .body("items.username", Matchers.hasItem("tam1")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/support").then() - .statusCode(303).header("Location", Matchers.endsWith("/support/tickets")); + RestAssured.given().redirects().follow(false).get("/support").then().statusCode(303).header("Location", + Matchers.endsWith("/support/tickets")); ai.mnemosyne_systems.model.User supportUser = ai.mnemosyne_systems.model.User .find("email", "support1@mnemosyne-systems.ai").firstResult(); int assignedCount = ai.mnemosyne_systems.model.Ticket.find( @@ -80,28 +89,25 @@ void supportCanAccessSupportUsersMenu() { supportUser).list().size(); int openCount = ai.mnemosyne_systems.model.Ticket .find("select distinct t from Ticket t where t.supportUsers is empty").list().size(); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("pageSize", 100) - .get("/api/support/tickets").then().statusCode(200).body("title", Matchers.equalTo("Tickets")) - .body("assignedCount", Matchers.equalTo(assignedCount)).body("openCount", Matchers.equalTo(openCount)) - .body("items.name", Matchers.hasItem(supportTicketName)) + RestAssured.given().queryParam("pageSize", 100).get("/api/support/tickets").then().statusCode(200) + .body("title", Matchers.equalTo("Tickets")).body("assignedCount", Matchers.equalTo(assignedCount)) + .body("openCount", Matchers.equalTo(openCount)).body("items.name", Matchers.hasItem(supportTicketName)) .body("items.status", Matchers.hasItem("Assigned")) .body("items.supportUser.username", Matchers.hasItem("support1")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/support/open").then() - .statusCode(303).header("Location", Matchers.endsWith("/support/tickets/open")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("view", "open") - .get("/api/support/tickets").then().statusCode(200).body("title", Matchers.equalTo("Open tickets")) + RestAssured.given().redirects().follow(false).get("/support/open").then().statusCode(303).header("Location", + Matchers.endsWith("/support/tickets/open")); + RestAssured.given().queryParam("view", "open").get("/api/support/tickets").then().statusCode(200) + .body("title", Matchers.equalTo("Open tickets")) .body("items.name", Matchers.hasItems("A-00002", "A-00003")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/support/closed") - .then().statusCode(303).header("Location", Matchers.endsWith("/support/tickets/closed")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("view", "closed") - .get("/api/support/tickets").then().statusCode(200).body("title", Matchers.equalTo("Closed tickets")) - .body("items.name", Matchers.hasItem("A-00004")); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/support/tickets/create").then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/support/closed").then().statusCode(303).header("Location", + Matchers.endsWith("/support/tickets/closed")); + RestAssured.given().queryParam("view", "closed").get("/api/support/tickets").then().statusCode(200) + .body("title", Matchers.equalTo("Closed tickets")).body("items.name", Matchers.hasItem("A-00004")); + + RestAssured.given().redirects().follow(false).get("/support/tickets/create").then().statusCode(303) .header("Location", Matchers.endsWith("/support/tickets/new")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/support/tickets/bootstrap").then() - .statusCode(200).body("submitPath", Matchers.equalTo("/support/tickets")) + RestAssured.given().get("/api/support/tickets/bootstrap").then().statusCode(200) + .body("submitPath", Matchers.equalTo("/support/tickets")) .body("categories.name", Matchers.hasItem("Question")) .body("companyEntitlements.size()", Matchers.greaterThanOrEqualTo(1)) .body("ticketName", Matchers.not(Matchers.isEmptyOrNullString())); @@ -112,8 +118,7 @@ void supportCanAccessSupportUsersMenu() { .find("entitlement = ?1 order by date asc, id asc", supportCreateEntitlement.entitlement).firstResult(); Assertions.assertNotNull(supportCreateVersion); String supportCreateMessage = "Support create redirect coverage"; - String supportCreateRedirect = RestAssured.given().redirects().follow(false) - .cookie(AuthHelper.AUTH_COOKIE, cookie).multiPart("status", "Open") + String supportCreateRedirect = RestAssured.given().redirects().follow(false).multiPart("status", "Open") .multiPart("title", "Support create redirect title").multiPart("message", supportCreateMessage) .multiPart("companyId", companyId).multiPart("companyEntitlementId", supportCreateEntitlement.id) .multiPart("categoryId", Category.findDefault().id) @@ -128,92 +133,107 @@ void supportCanAccessSupportUsersMenu() { Assertions.assertTrue(supportCreateRedirect.endsWith("/support/tickets/" + createdSupportTicket.id)); Long ticketId = supportTicket == null ? null : supportTicket.id; - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/tickets/" + ticketId + "/edit").then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/tickets/" + ticketId + "/edit").then().statusCode(303) .header("Location", Matchers.endsWith("/tickets/" + ticketId + "/edit")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/ticket-workbench/" + ticketId).then() - .statusCode(200).body("entitlements.name", Matchers.hasItem(Matchers.containsString("Starter"))) + RestAssured.given().get("/api/ticket-workbench/" + ticketId).then().statusCode(200) + .body("entitlements.name", Matchers.hasItem(Matchers.containsString("Starter"))) .body("messages.body", Matchers.hasItem("Sample ticket created.")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/tickets/1/edit") - .then().statusCode(303).header("Location", Matchers.endsWith("/tickets/1/edit")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/ticket-workbench/1").then().statusCode(200) - .body("ticket.externalIssueLink", - Matchers.equalTo("https://github.com/mnemosyne-systems/billetsys/issues/6")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie) - .queryParam("url", "https://github.com/mnemosyne-systems/billetsys/issues/6") + RestAssured.given().redirects().follow(false).get("/tickets/1/edit").then().statusCode(303).header("Location", + Matchers.endsWith("/tickets/1/edit")); + RestAssured.given().get("/api/ticket-workbench/1").then().statusCode(200).body("ticket.externalIssueLink", + Matchers.equalTo("https://github.com/mnemosyne-systems/billetsys/issues/6")); + RestAssured.given().queryParam("url", "https://github.com/mnemosyne-systems/billetsys/issues/6") .get("/tickets/external-preview").then().statusCode(200).body(Matchers .containsString("https://github.com/mnemosyne-systems/billetsys/issues/6")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/support/tickets/" + ticketId).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/support/tickets/" + ticketId).then().statusCode(303) .header("Location", Matchers.endsWith("/support/tickets/" + ticketId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/support/tickets/" + ticketId).then() - .statusCode(200).body("messages.body", Matchers.hasItem("Sample ticket created.")) + RestAssured.given().get("/api/support/tickets/" + ticketId).then().statusCode(200) + .body("messages.body", Matchers.hasItem("Sample ticket created.")) .body("supportUsers.username", Matchers.hasItem("support1")) .body("tamUsers.username", Matchers.hasItem("tam1")).body("companyName", Matchers.equalTo("Support Co")) .body("entitlementName", Matchers.not(Matchers.isEmptyOrNullString())) .body("levelName", Matchers.equalTo("Normal")).body("categoryName", Matchers.nullValue()) .body("statusOptions", Matchers.hasItems("Open", "Assigned", "In Progress", "Resolved", "Closed")); ai.mnemosyne_systems.model.Ticket unassignedTicket = ensureUnassignedOpenTicket(companyId); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("body", "Auto assign on support reply") + RestAssured.given().redirects().follow(false).multiPart("body", "Auto assign on support reply") .post("/support/tickets/" + unassignedTicket.id + "/messages").then().statusCode(303); Ticket autoAssignedTicket = refreshedTicket(unassignedTicket.id); Assertions.assertEquals("Assigned", autoAssignedTicket.status); Assertions.assertTrue(ticketHasSupportUser(unassignedTicket.id, supportUser.id)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("title", supportTicket.displayTitle()) - .formParam("status", "Assigned").formParam("companyId", supportTicket.company.id) + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC) + .formParam("title", supportTicket.displayTitle()).formParam("status", "Assigned") + .formParam("companyId", supportTicket.company.id) .formParam("companyEntitlementId", supportTicket.companyEntitlement.id) .formParam("affectsVersionId", ensureVersion(supportTicket.companyEntitlement.entitlement, "1.0.0").id) .post("/support/tickets/" + ticketId).then().statusCode(303); Ticket updatedSupportTicket = refreshedTicket(ticketId); Assertions.assertEquals("Assigned", updatedSupportTicket.status); Assertions.assertTrue(ticketHasSupportUser(ticketId, supportUser.id)); - String userCookie = login("user", "user"); - String tamCookie = login("tam1", "tam1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, userCookie) - .get("/tickets/" + ticketId).then().statusCode(303) - .header("Location", Matchers.endsWith("/user/tickets/" + ticketId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/api/user/tickets/" + ticketId).then() - .statusCode(200).body("displayStatus", Matchers.equalTo("Assigned")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, tamCookie) - .get("/tickets/" + ticketId).then().statusCode(303) - .header("Location", Matchers.endsWith("/user/tickets/" + ticketId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, tamCookie).get("/api/user/tickets/" + ticketId).then() - .statusCode(200).body("displayStatus", Matchers.equalTo("Assigned")); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/profile").then() - .statusCode(303).header("Location", Matchers.endsWith("/profile")); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/profile/password") - .then().statusCode(303).header("Location", Matchers.endsWith("/profile/password")); - } - - @Test - void articlesRespectRolePermissions() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + + RestAssured.given().redirects().follow(false).get("/profile").then().statusCode(303).header("Location", + Matchers.endsWith("/profile")); + RestAssured.given().redirects().follow(false).get("/profile/password").then().statusCode(303).header("Location", + Matchers.endsWith("/profile/password")); + } + + @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void userTicketRedirectWorksForUserRole() { + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + Long companyId = ensureCompany("Support Co User View"); + ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); + + RestAssured.given().redirects().follow(false).get("/tickets/" + ticket.id).then().statusCode(303) + .header("Location", Matchers.endsWith("/user/tickets/" + ticket.id)); + RestAssured.given().get("/api/user/tickets/" + ticket.id).then().statusCode(200).body("displayStatus", + Matchers.equalTo("Assigned")); + } + + @Test + @TestSecurity(user = "tam1", roles = "tam") + @JwtSecurity(claims = { @Claim(key = "email", value = "tam1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "tam") }) + void userTicketRedirectWorksForTamRole() { + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + Long companyId = ensureCompany("Support Co TAM View"); + ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); + + RestAssured.given().redirects().follow(false).get("/tickets/" + ticket.id).then().statusCode(303) + .header("Location", Matchers.endsWith("/user/tickets/" + ticket.id)); + RestAssured.given().get("/api/user/tickets/" + ticket.id).then().statusCode(200).body("displayStatus", + Matchers.equalTo("Assigned")); + } + + @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) + void supportCanCreateAndViewArticles() { + // [CHANGE] ensureUser calls — removed password argument + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); Article seeded = ensureArticle("Runbook", "ops,prod", "Seed article"); - String supportCookie = login("support1", "support1"); - String tamCookie = login("tam1", "tam1"); - String userCookie = login("user", "user"); - String adminCookie = login("admin", "admin"); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie).get("/articles") - .then().statusCode(303).header("Location", Matchers.endsWith("/articles")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .get("/articles/create").then().statusCode(303).header("Location", Matchers.endsWith("/articles/new")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .contentType("multipart/form-data").multiPart("title", "Article One").multiPart("tags", "guide") + + RestAssured.given().redirects().follow(false).get("/articles").then().statusCode(303).header("Location", + Matchers.endsWith("/articles")); + RestAssured.given().redirects().follow(false).get("/articles/create").then().statusCode(303).header("Location", + Matchers.endsWith("/articles/new")); + RestAssured.given().redirects().follow(false).contentType("multipart/form-data") + .multiPart("title", "Article One").multiPart("tags", "guide") .multiPart("body", "![pic](attachment://article.png)\n\n**Important**") .multiPart("attachments", "article.png", "img".getBytes(StandardCharsets.UTF_8), "image/png") .post("/articles").then().statusCode(303); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .contentType("multipart/form-data").multiPart("title", " article one ").multiPart("tags", "duplicate") + RestAssured.given().redirects().follow(false).contentType("multipart/form-data") + .multiPart("title", " article one ").multiPart("tags", "duplicate") .multiPart("body", "Duplicate body").post("/articles").then().statusCode(400) .body(Matchers.containsString("Article title already exists")); @@ -221,57 +241,88 @@ void articlesRespectRolePermissions() { Assertions.assertNotNull(created); Assertions.assertTrue(created.body.contains("/attachments/")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, tamCookie).get("/articles/create") - .then().statusCode(303).header("Location", Matchers.endsWith("/articles/new")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, userCookie) - .get("/articles/" + created.id).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/articles/" + created.id).then().statusCode(303) .header("Location", Matchers.endsWith("/articles/" + created.id)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .get("/articles/" + created.id).then().statusCode(303) + } + + @Test + @TestSecurity(user = "tam1", roles = "tam") + @JwtSecurity(claims = { @Claim(key = "email", value = "tam1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "tam") }) + void tamCanCreateAndViewArticles() { + // [CHANGE] ensureUser calls — removed password argument + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + Article seeded = ensureArticle("Runbook TAM", "ops", "Seed article"); + + RestAssured.given().redirects().follow(false).get("/articles/create").then().statusCode(303).header("Location", + Matchers.endsWith("/articles/new")); + + // TAM can also update an existing article; duplicate title check applies + Article existing = ensureArticle("Article One TAM", "guide", "body"); + RestAssured.given().redirects().follow(false).contentType("multipart/form-data") + .multiPart("title", "Runbook TAM").multiPart("tags", "ops").multiPart("body", "Updated body") + .post("/articles/" + existing.id).then().statusCode(400) + .body(Matchers.containsString("Article title already exists")); + } + + @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void userCanViewButNotCreateArticles() { + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + Article created = ensureArticle("Article For User", "guide", "body"); + + RestAssured.given().redirects().follow(false).get("/articles/" + created.id).then().statusCode(303) .header("Location", Matchers.endsWith("/articles/" + created.id)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/articles/create") - .then().statusCode(303); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, adminCookie).get("/articles") - .then().statusCode(303).header("Location", Matchers.endsWith("/articles")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, adminCookie) - .get("/articles/create").then().statusCode(303); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, adminCookie) - .get("/articles/" + seeded.id).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/articles/create").then().statusCode(303); + } + + @Test + @TestSecurity(user = "admin", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) + void adminCanViewAndDeleteArticles() { + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + Article seeded = ensureArticle("Runbook Admin", "ops,prod", "Seed article"); + + RestAssured.given().redirects().follow(false).get("/articles").then().statusCode(303).header("Location", + Matchers.endsWith("/articles")); + RestAssured.given().redirects().follow(false).get("/articles/create").then().statusCode(303); + RestAssured.given().redirects().follow(false).get("/articles/" + seeded.id).then().statusCode(303) .header("Location", Matchers.endsWith("/articles/" + seeded.id)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, tamCookie) - .contentType("multipart/form-data").multiPart("title", "Runbook").multiPart("tags", "ops") - .multiPart("body", "Updated body").post("/articles/" + created.id).then().statusCode(400) - .body(Matchers.containsString("Article title already exists")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, adminCookie) - .post("/articles/" + seeded.id + "/delete").then().statusCode(303); + RestAssured.given().redirects().follow(false).post("/articles/" + seeded.id + "/delete").then().statusCode(303); Assertions.assertNull(refreshedArticle(seeded.id)); } @Test + @TestSecurity(user = "support3", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support3@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support-003") }) void supportCanManageTicketsAndMessages() { - ensureUser("support3", "support3@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support3"); + ensureUser("support3", "support3@mnemosyne-systems.ai", User.TYPE_SUPPORT); ensureDefaultCategories(); - String cookie = login("support3", "support3"); + Long companyId = ensureCompany("Support CRUD Co"); Company company = Company.findById(companyId); Entitlement entitlement = ensureEntitlement("Starter", "Email support"); Level level = ensureLevel("Normal", "Normal response level", 1440, "White"); CompanyEntitlement entry = ensureCompanyEntitlement(company, entitlement, level); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("status", "Open").formParam("title", "CRUD create title") - .formParam("companyId", company.id).formParam("companyEntitlementId", entry.id).post("/tickets").then() - .statusCode(303); + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("status", "Open") + .formParam("title", "CRUD create title").formParam("companyId", company.id) + .formParam("companyEntitlementId", entry.id).post("/tickets").then().statusCode(303); Ticket ticket = Ticket.find("company = ?1 order by id desc", company).firstResult(); Assertions.assertNotNull(ticket); Assertions.assertNotNull(ticket.category); Assertions.assertEquals("Question", ticket.category.name); Category bugCategory = Category.find("name", "Bug").firstResult(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("status", "In Progress").formParam("companyId", company.id) - .formParam("companyEntitlementId", entry.id).formParam("categoryId", bugCategory.id) + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("status", "In Progress") + .formParam("companyId", company.id).formParam("companyEntitlementId", entry.id) + .formParam("categoryId", bugCategory.id) .formParam("affectsVersionId", ensureVersion(entry.entitlement, "1.0.0").id) .formParam("externalIssueLink", "https://github.com/example/issue/1").post("/tickets/" + ticket.id) .then().statusCode(303); @@ -280,26 +331,23 @@ void supportCanManageTicketsAndMessages() { Assertions.assertNotNull(updatedTicket.category); Assertions.assertEquals("Bug", updatedTicket.category.name); Assertions.assertEquals("https://github.com/example/issue/1", updatedTicket.externalIssueLink); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/tickets").then() - .statusCode(303).header("Location", Matchers.endsWith("/tickets")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/ticket-workbench").then().statusCode(200) - .body("items.name", Matchers.hasItem(ticket.name)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/tickets/new").then() - .statusCode(303).header("Location", Matchers.endsWith("/tickets/new")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/ticket-workbench/bootstrap").then() - .statusCode(200).body("title", Matchers.equalTo("New ticket")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/tickets/" + ticket.id + "/edit").then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/tickets").then().statusCode(303).header("Location", + Matchers.endsWith("/tickets")); + RestAssured.given().get("/api/ticket-workbench").then().statusCode(200).body("items.name", + Matchers.hasItem(ticket.name)); + RestAssured.given().redirects().follow(false).get("/tickets/new").then().statusCode(303).header("Location", + Matchers.endsWith("/tickets/new")); + RestAssured.given().get("/api/ticket-workbench/bootstrap").then().statusCode(200).body("title", + Matchers.equalTo("New ticket")); + RestAssured.given().redirects().follow(false).get("/tickets/" + ticket.id + "/edit").then().statusCode(303) .header("Location", Matchers.endsWith("/tickets/" + ticket.id + "/edit")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/ticket-workbench/" + ticket.id).then() - .statusCode(200) + RestAssured.given().get("/api/ticket-workbench/" + ticket.id).then().statusCode(200) .body("ticket.externalIssueLink", Matchers.equalTo("https://github.com/example/issue/1")); byte[] attachmentData = "Attachment line one\nAttachment line two".getBytes(StandardCharsets.UTF_8); byte[] attachmentDataTwo = "Second attachment".getBytes(StandardCharsets.UTF_8); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("body", "Support note").multiPart("date", "2024-01-01T10:00") - .multiPart("ticketId", String.valueOf(ticket.id)) + RestAssured.given().redirects().follow(false).multiPart("body", "Support note") + .multiPart("date", "2024-01-01T10:00").multiPart("ticketId", String.valueOf(ticket.id)) .multiPart("attachments", "note.txt", attachmentData, "text/plain") .multiPart("attachments", "note-2.txt", attachmentDataTwo, "text/plain") .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303); @@ -315,24 +363,21 @@ void supportCanManageTicketsAndMessages() { Assertions.assertEquals("note-2.txt", secondAttachment.name); Assertions.assertEquals(attachmentDataTwo.length, secondAttachment.data.length); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/support/tickets/" + ticket.id).then() - .statusCode(200) + RestAssured.given().get("/api/support/tickets/" + ticket.id).then().statusCode(200) .body("messages.attachments.name.flatten()", Matchers.hasItems("note.txt", "note-2.txt")) .body("messages.attachments.mimeType.flatten()", Matchers.hasItem("text/plain")) .body("categoryName", Matchers.equalTo("Bug")) .body("externalIssueLink", Matchers.equalTo("https://github.com/example/issue/1")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/attachments/" + attachment.id).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/attachments/" + attachment.id).then().statusCode(303) .header("Location", Matchers.endsWith("/attachments/" + attachment.id)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/attachments/" + attachment.id).then() - .statusCode(200).body("name", Matchers.equalTo("note.txt")) + RestAssured.given().get("/api/attachments/" + attachment.id).then().statusCode(200) + .body("name", Matchers.equalTo("note.txt")) .body("lines.content", Matchers.hasItem("Attachment line two")); byte[] replyData = "Reply attachment".getBytes(StandardCharsets.UTF_8); byte[] replyDataTwo = "Second reply attachment".getBytes(StandardCharsets.UTF_8); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("body", "Reply with attachments") + RestAssured.given().redirects().follow(false).multiPart("body", "Reply with attachments") .multiPart("attachments", "reply.txt", replyData, "text/plain") .multiPart("attachments", "reply-2.txt", replyDataTwo, "text/plain") .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303); @@ -344,43 +389,44 @@ void supportCanManageTicketsAndMessages() { Assertions.assertEquals("reply.txt", replyAttachments.get(0).name); Assertions.assertEquals("reply-2.txt", replyAttachments.get(1).name); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .post("/tickets/" + ticket.id + "/delete").then().statusCode(303); + RestAssured.given().redirects().follow(false).post("/tickets/" + ticket.id + "/delete").then().statusCode(303); Assertions.assertNull(refreshedTicket(ticket.id)); } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void supportTicketSearchMatchesTicketNumberAndMessages() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); Long companyId = ensureCompany("Support Search Co"); Ticket ticket = ensureTicket(companyId); String messageBody = "support-search-message-" + System.nanoTime(); ensureMessageWithBody(ticket, messageBody); - String cookie = login("support1", "support1"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("q", ticket.name) - .get("/api/support/tickets").then().statusCode(200).body("searchTerm", Matchers.equalTo(ticket.name)) - .body("items.name", Matchers.hasItem(ticket.name)); + RestAssured.given().queryParam("q", ticket.name).get("/api/support/tickets").then().statusCode(200) + .body("searchTerm", Matchers.equalTo(ticket.name)).body("items.name", Matchers.hasItem(ticket.name)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("q", messageBody) - .get("/api/support/tickets").then().statusCode(200).body("searchTerm", Matchers.equalTo(messageBody)) - .body("items.name", Matchers.hasItem(ticket.name)); + RestAssured.given().queryParam("q", messageBody).get("/api/support/tickets").then().statusCode(200) + .body("searchTerm", Matchers.equalTo(messageBody)).body("items.name", Matchers.hasItem(ticket.name)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("q", "no-support-ticket-match") - .get("/api/support/tickets").then().statusCode(200).body("items.size()", Matchers.equalTo(0)); + RestAssured.given().queryParam("q", "no-support-ticket-match").get("/api/support/tickets").then() + .statusCode(200).body("items.size()", Matchers.equalTo(0)); } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void supportTicketDetailUpdatesFieldsAndAddsRepliesWithAndWithoutAttachments() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); ensureDefaultCategories(); Long companyId = ensureCompany("Support Detail Flow Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureTicket(companyId); ensureMessage(ticket, "Original support detail message"); - String cookie = login("support1", "support1"); Category bugCategory = Category.find("name", "Bug").firstResult(); Assertions.assertNotNull(bugCategory); @@ -390,9 +436,9 @@ void supportTicketDetailUpdatesFieldsAndAddsRepliesWithAndWithoutAttachments() { java.time.LocalDate.of(2024, 3, 1)); String externalIssueLink = "https://github.com/mnemosyne-systems/billetsys/issues/6"; - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("title", ticket.displayTitle()) - .formParam("status", "Resolved").formParam("companyId", ticket.company.id) + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC) + .formParam("title", ticket.displayTitle()).formParam("status", "Resolved") + .formParam("companyId", ticket.company.id) .formParam("companyEntitlementId", ticket.companyEntitlement.id).formParam("categoryId", bugCategory.id) .formParam("externalIssueLink", externalIssueLink).formParam("affectsVersionId", affectsVersion.id) .formParam("resolvedVersionId", resolvedVersion.id).post("/support/tickets/" + ticket.id).then() @@ -405,8 +451,8 @@ void supportTicketDetailUpdatesFieldsAndAddsRepliesWithAndWithoutAttachments() { Assertions.assertEquals(affectsVersion.id, updatedTicket.affectsVersion.id); Assertions.assertEquals(resolvedVersion.id, updatedTicket.resolvedVersion.id); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/support/tickets/" + ticket.id).then() - .statusCode(200).body("actionPath", Matchers.equalTo("/support/tickets/" + ticket.id)) + RestAssured.given().get("/api/support/tickets/" + ticket.id).then().statusCode(200) + .body("actionPath", Matchers.equalTo("/support/tickets/" + ticket.id)) .body("messageActionPath", Matchers.equalTo("/support/tickets/" + ticket.id + "/messages")) .body("displayStatus", Matchers.equalTo("Resolved")).body("categoryName", Matchers.equalTo("Bug")) .body("externalIssueLink", Matchers.equalTo(externalIssueLink)) @@ -415,9 +461,8 @@ void supportTicketDetailUpdatesFieldsAndAddsRepliesWithAndWithoutAttachments() { .body("messages.body", Matchers.hasItem("Original support detail message")); String plainReplyBody = "Support reply without attachments " + System.nanoTime(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("body", plainReplyBody).post("/support/tickets/" + ticket.id + "/messages").then() - .statusCode(303) + RestAssured.given().redirects().follow(false).multiPart("body", plainReplyBody) + .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303) .header("Location", Matchers.endsWith("/support/tickets/" + ticket.id + "?replyAdded=1")); Message plainReply = Message.find("ticket = ?1 and body = ?2", ticket, plainReplyBody).firstResult(); @@ -426,8 +471,7 @@ void supportTicketDetailUpdatesFieldsAndAddsRepliesWithAndWithoutAttachments() { byte[] replyAttachmentData = "Support detail attachment".getBytes(StandardCharsets.UTF_8); String attachmentReplyBody = "Support reply with attachments " + System.nanoTime(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("body", attachmentReplyBody) + RestAssured.given().redirects().follow(false).multiPart("body", attachmentReplyBody) .multiPart("attachments", "detail-reply.txt", replyAttachmentData, "text/plain") .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303) .header("Location", Matchers.endsWith("/support/tickets/" + ticket.id + "?replyAdded=1")); @@ -440,54 +484,84 @@ void supportTicketDetailUpdatesFieldsAndAddsRepliesWithAndWithoutAttachments() { Assertions.assertEquals("text/plain", replyAttachments.get(0).mimeType); Assertions.assertEquals(replyAttachmentData.length, replyAttachments.get(0).data.length); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/support/tickets/" + ticket.id).then() - .statusCode(200).body("messages.body", Matchers.hasItems(plainReplyBody, attachmentReplyBody)) + RestAssured.given().get("/api/support/tickets/" + ticket.id).then().statusCode(200) + .body("messages.body", Matchers.hasItems(plainReplyBody, attachmentReplyBody)) .body("messages.attachments.name.flatten()", Matchers.hasItem("detail-reply.txt")) .body("messages.attachments.mimeType.flatten()", Matchers.hasItem("text/plain")); } @Test - void attachmentsPageUsesRoleHeader() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - Long companyId = ensureCompany("Attachment Role Co"); + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) + void attachmentsPageUsesRoleHeaderForSupport() { + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + Long companyId = ensureCompany("Attachment Role Co Support"); ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureTicket(companyId); Message message = ensureMessageWithBody(ticket, "Attachment role message"); Attachment attachment = ensureAttachment(message, "role-attachment.txt"); - String supportCookie = login("support1", "support1"); User supportUser = User.find("email", "support1@mnemosyne-systems.ai").firstResult(); SupportResource.SupportTicketCounts counts = SupportResource.loadTicketCounts(supportUser); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .get("/attachments/" + attachment.id).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/attachments/" + attachment.id).then().statusCode(303) .header("Location", Matchers.endsWith("/attachments/" + attachment.id)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, supportCookie).get("/api/attachments/" + attachment.id) - .then().statusCode(200).body("backPath", Matchers.equalTo("/support/tickets/" + ticket.id)); + RestAssured.given().get("/api/attachments/" + attachment.id).then().statusCode(200).body("backPath", + Matchers.equalTo("/support/tickets/" + ticket.id)); + } - String tamCookie = login("tam1", "tam1"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, tamCookie).get("/api/attachments/" + attachment.id).then() - .statusCode(200).body("backPath", Matchers.equalTo("/user/tickets/" + ticket.id)); + @Test + @TestSecurity(user = "tam1", roles = "tam") + @JwtSecurity(claims = { @Claim(key = "email", value = "tam1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "tam") }) + void attachmentsPageUsesRoleHeaderForTam() { + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + Long companyId = ensureCompany("Attachment Role Co TAM"); + ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); + Message message = ensureMessageWithBody(ticket, "Attachment role message tam"); + Attachment attachment = ensureAttachment(message, "role-attachment-tam.txt"); - String userCookie = login("user", "user"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/api/attachments/" + attachment.id).then() - .statusCode(200).body("backPath", Matchers.equalTo("/user/tickets/" + ticket.id)); + RestAssured.given().get("/api/attachments/" + attachment.id).then().statusCode(200).body("backPath", + Matchers.equalTo("/user/tickets/" + ticket.id)); } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void attachmentsPageUsesRoleHeaderForUser() { + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + Long companyId = ensureCompany("Attachment Role Co User"); + ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai"); + Ticket ticket = ensureTicket(companyId); + Message message = ensureMessageWithBody(ticket, "Attachment role message user"); + Attachment attachment = ensureAttachment(message, "role-attachment-user.txt"); + + RestAssured.given().get("/api/attachments/" + attachment.id).then().statusCode(200).body("backPath", + Matchers.equalTo("/user/tickets/" + ticket.id)); + } + + @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void sendsEmailsForTicketMessageAndStatusChanges() { mailbox.clear(); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); Long companyId = ensureCompany("Email Notify Co"); ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai", "user@mnemosyne-systems.ai"); Ticket ticket = ensureUnassignedOpenTicket(companyId); - String supportCookie = login("support1", "support1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .multiPart("body", "Email notification reply " + System.nanoTime()) + RestAssured.given().redirects().follow(false).multiPart("body", "Email notification reply " + System.nanoTime()) .multiPart("attachments", "notify.txt", "notify".getBytes(StandardCharsets.UTF_8), "text/plain") .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303); @@ -498,8 +572,8 @@ void sendsEmailsForTicketMessageAndStatusChanges() { Assertions.assertTrue(firstMail.getText().contains("Message")); Assertions.assertEquals(1, firstMail.getAttachments().size()); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .contentType(ContentType.URLENC).formParam("title", ticket.displayTitle()).formParam("status", "Closed") + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC) + .formParam("title", ticket.displayTitle()).formParam("status", "Closed") .formParam("companyId", ticket.company.id) .formParam("companyEntitlementId", ticket.companyEntitlement.id) .formParam("affectsVersionId", ensureVersion(ticket.companyEntitlement.entitlement, "1.0.0").id) @@ -513,22 +587,24 @@ void sendsEmailsForTicketMessageAndStatusChanges() { } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void sendsTicketNotificationsUsingEachRecipientsPreferredEmailFormat() { mailbox.clear(); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); setUserEmailFormat("support1@mnemosyne-systems.ai", "html"); setUserEmailFormat("tam1@mnemosyne-systems.ai", "text"); setUserEmailFormat("user@mnemosyne-systems.ai", null); Long companyId = ensureCompany("Per Recipient Email Format Co"); ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai", "user@mnemosyne-systems.ai"); Ticket ticket = ensureUnassignedOpenTicket(companyId); - String supportCookie = login("support1", "support1"); - String body = "Per recipient email format " + System.nanoTime(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .multiPart("body", body).post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303); + String body = "Per recipient email format " + System.nanoTime(); + RestAssured.given().redirects().follow(false).multiPart("body", body) + .post("/support/tickets/" + ticket.id + "/messages").then().statusCode(303); Mail htmlOnlyMail = latestMailTo("support1@mnemosyne-systems.ai"); Assertions.assertNull(htmlOnlyMail.getText()); @@ -548,12 +624,14 @@ void sendsTicketNotificationsUsingEachRecipientsPreferredEmailFormat() { } @Test - void sendsCorrectEmailsForTicketCreationAcrossRoles() { + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) + void sendsCorrectEmailForSupportTicketCreation() { mailbox.clear(); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + setUserEmailFormat("support1@mnemosyne-systems.ai", null); ensureDefaultCategories(); Entitlement supportEntitlement = ensureEntitlement("Support Create Email Entitlement", @@ -563,9 +641,9 @@ void sendsCorrectEmailsForTicketCreationAcrossRoles() { ensureCompanyUsers(supportCompanyId, "tam1@mnemosyne-systems.ai"); CompanyEntitlement supportCompanyEntitlement = ensureCompanyEntitlement(supportCompanyId, supportEntitlement); String supportBody = "Support create mail body " + System.nanoTime(); - String supportCookie = login("support1", "support1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie) - .multiPart("status", "Open").multiPart("title", "Support mail title").multiPart("message", supportBody) + + RestAssured.given().redirects().follow(false).multiPart("status", "Open") + .multiPart("title", "Support mail title").multiPart("message", supportBody) .multiPart("companyId", supportCompanyId) .multiPart("companyEntitlementId", supportCompanyEntitlement.id) .multiPart("categoryId", Category.findDefault().id).multiPart("affectsVersionId", supportVersion.id) @@ -573,6 +651,16 @@ void sendsCorrectEmailsForTicketCreationAcrossRoles() { Ticket createdSupportTicket = findMessageByBody(supportBody).ticket; assertTicketCreateMail(latestMailTo("support1@mnemosyne-systems.ai"), createdSupportTicket, "support1", "Open", supportBody); + } + + @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void sendsCorrectEmailForUserTicketCreation() { + mailbox.clear(); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureDefaultCategories(); Entitlement userEntitlement = ensureEntitlement("User Create Email Entitlement", "User create email assertions"); @@ -581,14 +669,24 @@ void sendsCorrectEmailsForTicketCreationAcrossRoles() { ensureCompanyUsers(userCompanyId, "user@mnemosyne-systems.ai"); CompanyEntitlement userCompanyEntitlement = ensureCompanyEntitlement(userCompanyId, userEntitlement); String userBody = "User create mail body " + System.nanoTime(); - String userCookie = login("user", "user"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, userCookie) - .multiPart("status", "Open").multiPart("title", "User mail title").multiPart("message", userBody) - .multiPart("companyId", userCompanyId).multiPart("companyEntitlementId", userCompanyEntitlement.id) + + RestAssured.given().redirects().follow(false).multiPart("status", "Open").multiPart("title", "User mail title") + .multiPart("message", userBody).multiPart("companyId", userCompanyId) + .multiPart("companyEntitlementId", userCompanyEntitlement.id) .multiPart("categoryId", Category.findDefault().id).multiPart("affectsVersionId", userVersion.id) .post("/user/tickets").then().statusCode(303); Ticket createdUserTicket = findMessageByBody(userBody).ticket; assertTicketCreateMail(latestMailTo("user@mnemosyne-systems.ai"), createdUserTicket, "user", "Open", userBody); + } + + @Test + @TestSecurity(user = "superuser1", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser") }) + void sendsCorrectEmailForSuperuserTicketCreation() { + mailbox.clear(); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + ensureDefaultCategories(); Entitlement superuserEntitlement = ensureEntitlement("Superuser Create Email Entitlement", "Superuser create email assertions"); @@ -598,10 +696,10 @@ void sendsCorrectEmailsForTicketCreationAcrossRoles() { CompanyEntitlement superuserCompanyEntitlement = ensureCompanyEntitlement(superuserCompanyId, superuserEntitlement); String superuserBody = "Superuser create mail body " + System.nanoTime(); - String superuserCookie = login("superuser1", "superuser1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, superuserCookie) - .multiPart("status", "Open").multiPart("title", "Superuser mail title") - .multiPart("message", superuserBody).multiPart("companyId", superuserCompanyId) + + RestAssured.given().redirects().follow(false).multiPart("status", "Open") + .multiPart("title", "Superuser mail title").multiPart("message", superuserBody) + .multiPart("companyId", superuserCompanyId) .multiPart("companyEntitlementId", superuserCompanyEntitlement.id) .multiPart("categoryId", Category.findDefault().id).multiPart("affectsVersionId", superuserVersion.id) .post("/superuser/tickets").then().statusCode(303); @@ -640,14 +738,16 @@ void reactAppSessionReturnsGuestStateWhenSignedOut() { } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void reactAppSessionReturnsRoleAwareLinksWhenSignedIn() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); setInstallationUse24HourClock(false); - String cookie = login("user", "user"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/app/session").then().statusCode(200) - .body("authenticated", Matchers.equalTo(true)).body("username", Matchers.equalTo("user")) - .body("role", Matchers.equalTo("user")).body("homePath", Matchers.equalTo("/user/tickets")) + RestAssured.given().get("/api/app/session").then().statusCode(200).body("authenticated", Matchers.equalTo(true)) + .body("username", Matchers.equalTo("user")).body("role", Matchers.equalTo("user")) + .body("homePath", Matchers.equalTo("/user/tickets")) .body("installationLogoBase64", Matchers.startsWith("data:image/svg+xml;base64,")) .body("installationBackgroundBase64", Matchers.anyOf(Matchers.nullValue(), Matchers.startsWith("data:image/"))) @@ -664,31 +764,24 @@ void reactAppSessionReturnsRoleAwareLinksWhenSignedIn() { } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void reactAppSessionReturnsSupportTicketLink() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - String cookie = login("support1", "support1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/app/session").then().statusCode(200) - .body("role", Matchers.equalTo("support")).body("homePath", Matchers.equalTo("/support/tickets")) + RestAssured.given().get("/api/app/session").then().statusCode(200).body("role", Matchers.equalTo("support")) + .body("homePath", Matchers.equalTo("/support/tickets")) .body("navigation.href", Matchers.hasItem("/support/tickets")) .body("inactivityTimeoutSeconds", Matchers.equalTo(AuthHelper.INACTIVITY_TIMEOUT_SECONDS)) .body("inactivityWarningSeconds", Matchers.equalTo(AuthHelper.WARNING_LEAD_SECONDS)); } - @Test - void reactLoginRedirectsSupportToSupportTickets() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - - RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("username", "support1") - .formParam("password", "support1").post("/login").then().statusCode(303) - .header("Location", Matchers.equalTo("/support/tickets")); - } - @Test void incomingEmailWithTicketSubjectAddsMessageAndAttachments() { mailbox.clear(); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); Long companyId = ensureCompany("Incoming Existing Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureTicket(companyId); @@ -713,7 +806,7 @@ void incomingEmailWithTicketSubjectAddsMessageAndAttachments() { @Test void incomingEmailWithoutTicketSubjectCreatesTicket() { mailbox.clear(); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); Long companyId = ensureCompany("Incoming New Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai"); ensureTicket(companyId); @@ -733,9 +826,9 @@ void incomingEmailWithoutTicketSubjectCreatesTicket() { @Test void incomingEmailIgnoresUnknownOrMismatchedFrom() { mailbox.clear(); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("other", "other@mnemosyne-systems.ai", User.TYPE_USER, "other"); - ensureUser("orphan", "orphan@mnemosyne-systems.ai", User.TYPE_USER, "orphan"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("other", "other@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("orphan", "orphan@mnemosyne-systems.ai", User.TYPE_USER); Long companyA = ensureCompany("Incoming Match Co"); Long companyB = ensureCompany("Incoming Other Co"); ensureCompanyUsers(companyA, "user@mnemosyne-systems.ai"); @@ -774,8 +867,8 @@ void incomingEmailRequiresBody() { @Test void incomingEmailNormalizesSenderAndAssignsCompanyTamsOnNewTicket() { mailbox.clear(); - ensureUser("normalize-user", "normalize-user@mnemosyne-systems.ai", User.TYPE_USER, "normalize-user"); - ensureUser("normalize-tam", "normalize-tam@mnemosyne-systems.ai", User.TYPE_TAM, "normalize-tam"); + ensureUser("normalize-user", "normalize-user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("normalize-tam", "normalize-tam@mnemosyne-systems.ai", User.TYPE_TAM); Long companyId = ensureCompany("Incoming Normalize Co"); ensureCompanyUsers(companyId, "normalize-user@mnemosyne-systems.ai", "normalize-tam@mnemosyne-systems.ai"); ensureTicket(companyId); @@ -797,8 +890,8 @@ void incomingEmailNormalizesSenderAndAssignsCompanyTamsOnNewTicket() { @Test void mailboxMessageWithTicketSubjectAddsMessageAndAttachments() throws Exception { mailbox.clear(); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); Long companyId = ensureCompany("Mailbox Existing Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureTicket(companyId); @@ -836,7 +929,7 @@ void mailboxMessageWithTicketSubjectAddsMessageAndAttachments() throws Exception @Test void mailboxMessageWithoutTicketSubjectCreatesTicket() throws Exception { mailbox.clear(); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); Long companyId = ensureCompany("Mailbox New Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai"); ensureTicket(companyId); @@ -860,7 +953,7 @@ void mailboxMessageWithoutTicketSubjectCreatesTicket() throws Exception { @Test void mailboxHtmlMessageStripsHtmlAndKeepsInlineAttachments() throws Exception { mailbox.clear(); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); Long companyId = ensureCompany("Mailbox Html Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai"); ensureTicket(companyId); @@ -908,36 +1001,38 @@ void computeEffectiveStatusUsesSupportAssignmentsAndOpenFallback() { } @Test + @TestSecurity(user = "support-json", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support-json@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support-002") }) void reactSupportMutationsReturnJsonRedirects() { - ensureUser("support-json", "support-json@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support-json"); + ensureUser("support-json", "support-json@mnemosyne-systems.ai", User.TYPE_SUPPORT); ensureDefaultCategories(); - String cookie = login("support-json", "support-json"); + Long companyId = ensureCompany("Support Json Co"); Company company = Company.findById(companyId); Entitlement entitlement = ensureEntitlement("Support Json Entitlement", "Support json detail"); Version version = ensureVersion(entitlement, "8.0.0", java.time.LocalDate.of(2026, 1, 1)); CompanyEntitlement entry = ensureCompanyEntitlement(companyId, entitlement); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .multiPart("status", "Open").multiPart("title", "Support json redirect title") - .multiPart("message", "Support json redirect create").multiPart("companyId", company.id) - .multiPart("companyEntitlementId", entry.id).multiPart("categoryId", Category.findDefault().id) - .multiPart("affectsVersionId", version.id).post("/support/tickets").then().statusCode(200) + RestAssured.given().header("X-Billetsys-Client", "react").multiPart("status", "Open") + .multiPart("title", "Support json redirect title").multiPart("message", "Support json redirect create") + .multiPart("companyId", company.id).multiPart("companyEntitlementId", entry.id) + .multiPart("categoryId", Category.findDefault().id).multiPart("affectsVersionId", version.id) + .post("/support/tickets").then().statusCode(200) .body("redirectTo", Matchers.matchesPattern("/support/tickets/\\d+")); Ticket createdTicket = findMessageByBody("Support json redirect create").ticket; Assertions.assertNotNull(createdTicket); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("title", createdTicket.displayTitle()) - .formParam("status", "Assigned").formParam("companyId", company.id) - .formParam("companyEntitlementId", entry.id).formParam("categoryId", Category.findDefault().id) - .formParam("affectsVersionId", version.id).post("/support/tickets/" + createdTicket.id).then() - .statusCode(200).body("redirectTo", Matchers.equalTo("/support/tickets/" + createdTicket.id)); + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC) + .formParam("title", createdTicket.displayTitle()).formParam("status", "Assigned") + .formParam("companyId", company.id).formParam("companyEntitlementId", entry.id) + .formParam("categoryId", Category.findDefault().id).formParam("affectsVersionId", version.id) + .post("/support/tickets/" + createdTicket.id).then().statusCode(200) + .body("redirectTo", Matchers.equalTo("/support/tickets/" + createdTicket.id)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") + RestAssured.given().header("X-Billetsys-Client", "react") .post("/support/tickets/" + createdTicket.id + "/assign").then().statusCode(200) .body("redirectTo", Matchers.equalTo("/support/tickets/" + createdTicket.id)); } - } diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/TAMAccessTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/TAMAccessTest.java index 6b49d761..0be6d38b 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/TAMAccessTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/TAMAccessTest.java @@ -45,90 +45,88 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class TAMAccessTest extends AccessTestSupport { @Test + @TestSecurity(user = "tam1", roles = "tam") + @JwtSecurity(claims = { @Claim(key = "email", value = "tam1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "tam1") }) void tamCanAccessUserTicketsMenu() { - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); Entitlement createPageEntitlement = ensureEntitlement("TAM Create Versions", "TAM create version list"); ensureVersion(createPageEntitlement, "8.8.8", java.time.LocalDate.of(2024, 6, 1)); Long companyId = ensureCompany("TAM Co"); ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai"); - ai.mnemosyne_systems.model.Ticket tamTicket = ensureTicket(companyId); + Ticket tamTicket = ensureTicket(companyId); String tamTicketName = tamTicket == null ? null : tamTicket.name; - String cookie = login("tam1", "tam1"); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/user/tickets").then() - .statusCode(303).header("Location", Matchers.endsWith("/user/tickets")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets").then().statusCode(200) - .body("items.supportUser.username", Matchers.hasItem("support1")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets/bootstrap").then() - .statusCode(200).body("submitPath", Matchers.equalTo("/user/tickets")) + + RestAssured.given().redirects().follow(false).get("/user/tickets").then().statusCode(303).header("Location", + Matchers.endsWith("/user/tickets")); + RestAssured.given().get("/api/user/tickets").then().statusCode(200).body("items.supportUser.username", + Matchers.hasItem("support1")); + RestAssured.given().get("/api/user/tickets/bootstrap").then().statusCode(200) + .body("submitPath", Matchers.equalTo("/user/tickets")) .body("defaultAffectsVersion.name", Matchers.equalTo("1.0.0")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/rss/tam").then().statusCode(200) + RestAssured.given().get("/rss/tam").then().statusCode(200) .contentType(Matchers.containsString("application/rss+xml")).body(Matchers.containsString(" entry.id.equals(tamCreated.id))); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, tamCookie) - .contentType(ContentType.URLENC).formParam("name", "TAM User") + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("name", "TAM User") .formParam("email", "tam-user-duplicate@mnemosyne-systems.ai").formParam("password", "tam-user") .formParam("type", "user").formParam("companyId", tamCompanyId).post("/tam/users").then() .statusCode(400); } @Test - void reportsRespectRolePermissions() { - ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "admin"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); + @TestSecurity(user = "admin", roles = "admin") + @JwtSecurity(claims = { @Claim(key = "email", value = "admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "admin") }) + void adminCanAccessReports() { + // [CHANGE] ensureUser — removed password argument + ensureUser("admin", "admin@mnemosyne-systems.ai", User.TYPE_ADMIN); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); Long companyId = ensureCompany("Report Co"); ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai"); ensureTicket(companyId); - String adminCookie = login("admin", "admin"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, adminCookie).get("/reports").then() - .statusCode(303).header("Location", Matchers.endsWith("/reports")); + RestAssured.given().redirects().follow(false).get("/reports").then().statusCode(303).header("Location", + Matchers.endsWith("/reports")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, adminCookie) - .queryParam("companyId", companyId).get("/reports").then().statusCode(303) - .header("Location", Matchers.endsWith("/reports?companyId=" + companyId)); + RestAssured.given().redirects().follow(false).queryParam("companyId", companyId).get("/reports").then() + .statusCode(303).header("Location", Matchers.endsWith("/reports?companyId=" + companyId)); + } - String tamCookie = login("tam1", "tam1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, tamCookie).get("/reports/tam") - .then().statusCode(303).header("Location", Matchers.endsWith("/reports")); + @Test + @TestSecurity(user = "tam1", roles = "tam") + @JwtSecurity(claims = { @Claim(key = "email", value = "tam1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "tam") }) + void tamCanAccessReports() { + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + Long companyId = ensureCompany("Report Co TAM"); + ensureCompanyUsers(companyId, "tam1@mnemosyne-systems.ai"); + ensureTicket(companyId); - String userCookie = login("user", "user"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/reports").then() - .statusCode(303); + RestAssured.given().redirects().follow(false).get("/reports/tam").then().statusCode(303).header("Location", + Matchers.endsWith("/reports")); + } - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, userCookie).get("/reports/tam") - .then().statusCode(303); + @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) + void userCannotAccessReports() { + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + Long companyId = ensureCompany("Report Co User"); + ensureTicket(companyId); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, tamCookie).get("/reports").then() - .statusCode(303); + RestAssured.given().redirects().follow(false).get("/reports").then().statusCode(403); + + RestAssured.given().redirects().follow(false).get("/reports/tam").then().statusCode(403); + } + + @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) + void supportCannotAccessReports() { + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + Long companyId = ensureCompany("Report Co Support"); + ensureTicket(companyId); - String supportCookie = login("support1", "support1"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, supportCookie).get("/reports") - .then().statusCode(303); + RestAssured.given().redirects().follow(false).get("/reports").then().statusCode(403); } } diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/TicketImportResourceTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/TicketImportResourceTest.java index ed32f045..3890810e 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/TicketImportResourceTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/TicketImportResourceTest.java @@ -24,14 +24,19 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class TicketImportResourceTest extends AccessTestSupport { @Test + @TestSecurity(user = "import-support", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "import-support@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "import-support") }) void supportUserCanImportCsvTicketsAndMessages() { ImportFixture fixture = fixture("import-success"); - String cookie = login("import-support", "pass"); String sourceKey = "csv-success-" + System.nanoTime(); String csv = """ source_key,title,company,entitlement,status,initial_message,requester_email,category,external_issue_link,created_at @@ -40,7 +45,7 @@ void supportUserCanImportCsvTicketsAndMessages() { """ .formatted(sourceKey, fixture.companyName(), fixture.entitlementName()); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType("multipart/form-data") + RestAssured.given().contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") .post("/api/ticket-imports/csv").then().statusCode(200).body("rowCount", Matchers.equalTo(1)) .body("createdCount", Matchers.equalTo(1)).body("skippedCount", Matchers.equalTo(0)) @@ -57,17 +62,19 @@ void supportUserCanImportCsvTicketsAndMessages() { } @Test + @TestSecurity(user = "import-admin", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "import-admin@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "import-admin") }) void adminUserCanImportCsvTicketsAndMessages() { ImportFixture fixture = fixture("import-admin-success"); - ensureUser("import-admin", "import-admin@mnemosyne-systems.ai", User.TYPE_ADMIN, "pass"); - String cookie = login("import-admin", "pass"); + ensureUser("import-admin", "import-admin@mnemosyne-systems.ai", User.TYPE_ADMIN); String sourceKey = "csv-admin-success-" + System.nanoTime(); String csv = """ source_key,title,company,entitlement,status,initial_message,requester_email,category %s,Imported by admin,%s,%s,Open,Imported from admin,import-user@mnemosyne-systems.ai,Bug """.formatted(sourceKey, fixture.companyName(), fixture.entitlementName()); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType("multipart/form-data") + RestAssured.given().contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") .post("/api/ticket-imports/csv").then().statusCode(200).body("rowCount", Matchers.equalTo(1)) .body("createdCount", Matchers.equalTo(1)).body("failedCount", Matchers.equalTo(0)); @@ -78,19 +85,21 @@ void adminUserCanImportCsvTicketsAndMessages() { } @Test + @TestSecurity(user = "import-support", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "import-support@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "import-support") }) void duplicateSourceKeyIsSkippedOnReimport() { ImportFixture fixture = fixture("import-duplicate"); - String cookie = login("import-support", "pass"); String sourceKey = "csv-duplicate-" + System.nanoTime(); String csv = """ source_key,title,company,entitlement,status,initial_message %s,Duplicate import,%s,%s,Open,Initial duplicate import """.formatted(sourceKey, fixture.companyName(), fixture.entitlementName()); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType("multipart/form-data") + RestAssured.given().contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") .post("/api/ticket-imports/csv").then().statusCode(200).body("createdCount", Matchers.equalTo(1)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType("multipart/form-data") + RestAssured.given().contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") .post("/api/ticket-imports/csv").then().statusCode(200).body("createdCount", Matchers.equalTo(0)) .body("skippedCount", Matchers.equalTo(1)).body("results[0].result", Matchers.equalTo("skipped")); @@ -100,16 +109,18 @@ void duplicateSourceKeyIsSkippedOnReimport() { } @Test + @TestSecurity(user = "import-support", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "import-support@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "import-support") }) void invalidRowsReturnRowLevelErrors() { fixture("import-invalid"); - String cookie = login("import-support", "pass"); String sourceKey = "csv-invalid-" + System.nanoTime(); String csv = """ source_key,title,company,entitlement,status,initial_message,category %s,Missing company,Does Not Exist,Starter,Open,Message,Bug """.formatted(sourceKey); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType("multipart/form-data") + RestAssured.given().contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") .post("/api/ticket-imports/csv").then().statusCode(200).body("createdCount", Matchers.equalTo(0)) .body("failedCount", Matchers.equalTo(1)).body("results[0].result", Matchers.equalTo("failed")) @@ -119,15 +130,17 @@ void invalidRowsReturnRowLevelErrors() { } @Test + @TestSecurity(user = "import-support", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "import-support@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "import-support") }) void missingSourceKeyReturnsRowLevelError() { ImportFixture fixture = fixture("import-missing-source-key"); - String cookie = login("import-support", "pass"); String csv = """ source_key,title,company,entitlement,status,initial_message ,Missing source key,%s,%s,Open,Message """.formatted(fixture.companyName(), fixture.entitlementName()); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType("multipart/form-data") + RestAssured.given().contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") .post("/api/ticket-imports/csv").then().statusCode(200).body("createdCount", Matchers.equalTo(0)) .body("failedCount", Matchers.equalTo(1)).body("results[0].result", Matchers.equalTo("failed")) @@ -135,37 +148,40 @@ void missingSourceKeyReturnsRowLevelError() { } @Test + @TestSecurity(user = "import-user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "import-user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "import-user") }) void nonSupportUserCannotImportCsv() { fixture("import-access"); - String cookie = login("import-user", "pass"); String csv = """ source_key,title,company,entitlement,status,initial_message cannot-import,Denied,Company,Starter,Open,Denied """; - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType("multipart/form-data") + RestAssured.given().redirects().follow(false).contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") - .post("/api/ticket-imports/csv").then().statusCode(303); + .post("/api/ticket-imports/csv").then().statusCode(403); } @Test + @TestSecurity(user = "import-support", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "import-support@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "import-support") }) void missingRequiredHeaderFailsClearly() { - ensureUser("import-support", "import-support@mnemosyne-systems.ai", User.TYPE_SUPPORT, "pass"); - String cookie = login("import-support", "pass"); + ensureUser("import-support", "import-support@mnemosyne-systems.ai", User.TYPE_SUPPORT); String csv = """ source_key,title,company,status,initial_message missing-entitlement,Missing entitlement,Company,Open,Message """; - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType("multipart/form-data") + RestAssured.given().contentType("multipart/form-data") .multiPart("file", "tickets.csv", csv.getBytes(StandardCharsets.UTF_8), "text/csv") .post("/api/ticket-imports/csv").then().statusCode(400).body(Matchers.containsString("entitlement")); } private ImportFixture fixture(String suffix) { - ensureUser("import-support", "import-support@mnemosyne-systems.ai", User.TYPE_SUPPORT, "pass"); - ensureUser("import-user", "import-user@mnemosyne-systems.ai", User.TYPE_USER, "pass"); + ensureUser("import-support", "import-support@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("import-user", "import-user@mnemosyne-systems.ai", User.TYPE_USER); ensureCategory("Bug", "Defects", false); Entitlement entitlement = ensureEntitlement("Starter Import " + suffix, "Import entitlement"); Long companyId = ensureCompany("Import Company " + suffix); diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/TicketRatingAccessTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/TicketRatingAccessTest.java index 0f56a1ac..d0c3bb84 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/TicketRatingAccessTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/TicketRatingAccessTest.java @@ -1,10 +1,10 @@ /* - * Eclipse Public License - v 2.0 - * - * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE - * PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION - * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - */ +* Eclipse Public License - v 2.0 +* +* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +* PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION +* OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. +*/ package ai.mnemosyne_systems.resource; @@ -17,26 +17,28 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class TicketRatingAccessTest extends AccessTestSupport { - @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void userCanRateOwnResolvedTicket() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating User Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - String cookie = login("user", "user"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("rating", 8).formParam("ratingComment", "Great support!") - .post("/tickets/" + ticket.id + "/rating").then().statusCode(200) - .body("redirectTo", Matchers.equalTo("")); + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC).formParam("rating", 8) + .formParam("ratingComment", "Great support!").post("/tickets/" + ticket.id + "/rating").then() + .statusCode(200).body("redirectTo", Matchers.equalTo("")); Ticket rated = refreshedTicket(ticket.id); Assertions.assertEquals(8, rated.rating); @@ -44,21 +46,22 @@ void userCanRateOwnResolvedTicket() { } @Test + @TestSecurity(user = "superuser1", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "superuser1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser") }) void superuserCanRateCompanyTicket() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "superuser1"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("superuser1", "superuser1@mnemosyne-systems.ai", User.TYPE_SUPERUSER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Superuser Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - String cookie = login("superuser1", "superuser1"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("rating", 10).formParam("ratingComment", "Excellent!") + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC) + .formParam("rating", 10).formParam("ratingComment", "Excellent!") .post("/tickets/" + ticket.id + "/rating").then().statusCode(200) .body("redirectTo", Matchers.equalTo("")); @@ -68,46 +71,48 @@ void superuserCanRateCompanyTicket() { } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support") }) void supportCannotSubmitRating() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Support Deny Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - String cookie = login("support1", "support1"); - - // Support role should be redirected to login (role not allowed) - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("rating", 7).post("/tickets/" + ticket.id + "/rating").then() - .statusCode(303).header("Location", Matchers.endsWith("/login")); + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("rating", 7) + .post("/tickets/" + ticket.id + "/rating").then().statusCode(403); } @Test + @TestSecurity(user = "tam1", roles = "tam") + @JwtSecurity(claims = { @Claim(key = "email", value = "tam1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "tam") }) void tamCannotSubmitRating() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating TAM Deny Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - String cookie = login("tam1", "tam1"); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("rating", 7).post("/tickets/" + ticket.id + "/rating").then() - .statusCode(303).header("Location", Matchers.endsWith("/login")); + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("rating", 7) + .post("/tickets/" + ticket.id + "/rating").then().statusCode(403); } @Test + @TestSecurity(user = "other-su", roles = "superuser") + @JwtSecurity(claims = { @Claim(key = "email", value = "other-su@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "superuser") }) void superuserCannotRateOtherCompanyTicket() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); - ensureUser("other-su", "other-su@mnemosyne-systems.ai", User.TYPE_SUPERUSER, "other-su"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); + ensureUser("other-su", "other-su@mnemosyne-systems.ai", User.TYPE_SUPERUSER); ensureDefaultCategories(); Long companyA = ensureCompany("Rating Company A"); ensureCompanyUsers(companyA, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); @@ -115,78 +120,79 @@ void superuserCannotRateOtherCompanyTicket() { ensureCompanyUsers(companyB, "other-su@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyA); - // Superuser from Company B tries to rate Company A's ticket - String cookie = login("other-su", "other-su"); - - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .contentType(ContentType.URLENC).formParam("rating", 5).post("/tickets/" + ticket.id + "/rating").then() - .statusCode(303).header("Location", Matchers.endsWith("/")); + RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("rating", 5) + .post("/tickets/" + ticket.id + "/rating").then().statusCode(303) + .header("Location", Matchers.endsWith("/")); } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void ratingBelowOneIsRejected() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Range Low Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - String cookie = login("user", "user"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType(ContentType.URLENC) - .formParam("rating", 0).post("/tickets/" + ticket.id + "/rating").then().statusCode(400); + RestAssured.given().contentType(ContentType.URLENC).formParam("rating", 0) + .post("/tickets/" + ticket.id + "/rating").then().statusCode(400); } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void ratingAboveTenIsRejected() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Range High Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - String cookie = login("user", "user"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType(ContentType.URLENC) - .formParam("rating", 11).post("/tickets/" + ticket.id + "/rating").then().statusCode(400); + RestAssured.given().contentType(ContentType.URLENC).formParam("rating", 11) + .post("/tickets/" + ticket.id + "/rating").then().statusCode(400); } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void cannotRateNonResolvedTicket() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Not Resolved Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureTicket(companyId); // status is "Assigned", not "Resolved" - String cookie = login("user", "user"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType(ContentType.URLENC) - .formParam("rating", 5).post("/tickets/" + ticket.id + "/rating").then().statusCode(400); + RestAssured.given().contentType(ContentType.URLENC).formParam("rating", 5) + .post("/tickets/" + ticket.id + "/rating").then().statusCode(400); } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void cannotRateAlreadyRatedTicket() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Already Rated Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); setTicketRating(ticket.id, 7, "Already rated"); - String cookie = login("user", "user"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).contentType(ContentType.URLENC) - .formParam("rating", 9).formParam("ratingComment", "Try again") - .post("/tickets/" + ticket.id + "/rating").then().statusCode(400); + RestAssured.given().contentType(ContentType.URLENC).formParam("rating", 9) + .formParam("ratingComment", "Try again").post("/tickets/" + ticket.id + "/rating").then() + .statusCode(400); // Verify original rating is unchanged Ticket unchanged = refreshedTicket(ticket.id); @@ -195,20 +201,20 @@ void cannotRateAlreadyRatedTicket() { } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void ratingWithoutCommentSetsCommentToNull() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating No Comment Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - String cookie = login("user", "user"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("rating", 5).formParam("ratingComment", "") - .post("/tickets/" + ticket.id + "/rating").then().statusCode(200); + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC).formParam("rating", 5) + .formParam("ratingComment", "").post("/tickets/" + ticket.id + "/rating").then().statusCode(200); Ticket rated = refreshedTicket(ticket.id); Assertions.assertEquals(5, rated.rating); @@ -216,54 +222,52 @@ void ratingWithoutCommentSetsCommentToNull() { } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void ratingIsVisibleInUserTicketDetailResponse() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Visible User Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); setTicketRating(ticket.id, 9, "Wonderful service"); - String cookie = login("user", "user"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets/" + ticket.id).then() - .statusCode(200).body("rating", Matchers.equalTo(9)) - .body("ratingComment", Matchers.equalTo("Wonderful service")); + RestAssured.given().get("/api/user/tickets/" + ticket.id).then().statusCode(200) + .body("rating", Matchers.equalTo(9)).body("ratingComment", Matchers.equalTo("Wonderful service")); } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support1") }) void ratingIsVisibleInSupportTicketDetailResponse() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Visible Support Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); setTicketRating(ticket.id, 4, "Could be better"); - String cookie = login("support1", "support1"); - - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/support/tickets/" + ticket.id).then() - .statusCode(200).body("rating", Matchers.equalTo(4)) - .body("ratingComment", Matchers.equalTo("Could be better")); + RestAssured.given().get("/api/support/tickets/" + ticket.id).then().statusCode(200) + .body("rating", Matchers.equalTo(4)).body("ratingComment", Matchers.equalTo("Could be better")); } @Test void unauthenticatedUserCannotRate() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("Rating Unauth Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); Ticket ticket = ensureResolvedTicket(companyId); - // No cookie — should redirect to login RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("rating", 5) - .post("/tickets/" + ticket.id + "/rating").then().statusCode(303) - .header("Location", Matchers.endsWith("/login")); + .post("/tickets/" + ticket.id + "/rating").then().statusCode(401); } } diff --git a/src/backend/test/java/ai/mnemosyne_systems/resource/UserAccessTest.java b/src/backend/test/java/ai/mnemosyne_systems/resource/UserAccessTest.java index 57e7be19..dad5ebd6 100644 --- a/src/backend/test/java/ai/mnemosyne_systems/resource/UserAccessTest.java +++ b/src/backend/test/java/ai/mnemosyne_systems/resource/UserAccessTest.java @@ -54,13 +54,19 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; @QuarkusTest class UserAccessTest extends AccessTestSupport { @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void userCanAccessUserTicketsMenu() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); ensureDefaultCategories(); Entitlement createPageEntitlement = ensureEntitlement("Create Page Versions", "Create page version list"); ensureVersion(createPageEntitlement, "0.9.0", java.time.LocalDate.of(2024, 1, 1)); @@ -69,22 +75,20 @@ void userCanAccessUserTicketsMenu() { ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "superuser1@mnemosyne-systems.ai"); ai.mnemosyne_systems.model.Ticket userTicket = ensureTicket(companyId); String userTicketName = userTicket == null ? "" : userTicket.name; - String cookie = login("user", "user"); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/user/tickets").then() - .statusCode(303).header("Location", Matchers.endsWith("/user/tickets")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/user/tickets/open") - .then().statusCode(303).header("Location", Matchers.endsWith("/user/tickets/open")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/user/tickets/closed") - .then().statusCode(303).header("Location", Matchers.endsWith("/user/tickets/closed")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/user/tickets/create") - .then().statusCode(303).header("Location", Matchers.endsWith("/user/tickets/new")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("pageSize", 100).get("/api/user/tickets") - .then().statusCode(200).body("title", Matchers.equalTo("Tickets")) - .body("createPath", Matchers.equalTo("/user/tickets/new")) + RestAssured.given().redirects().follow(false).get("/user/tickets").then().statusCode(303).header("Location", + Matchers.endsWith("/user/tickets")); + RestAssured.given().redirects().follow(false).get("/user/tickets/open").then().statusCode(303) + .header("Location", Matchers.endsWith("/user/tickets/open")); + RestAssured.given().redirects().follow(false).get("/user/tickets/closed").then().statusCode(303) + .header("Location", Matchers.endsWith("/user/tickets/closed")); + RestAssured.given().redirects().follow(false).get("/user/tickets/create").then().statusCode(303) + .header("Location", Matchers.endsWith("/user/tickets/new")); + RestAssured.given().queryParam("pageSize", 100).get("/api/user/tickets").then().statusCode(200) + .body("title", Matchers.equalTo("Tickets")).body("createPath", Matchers.equalTo("/user/tickets/new")) .body("items.name", Matchers.hasItem(userTicketName)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets/bootstrap").then() - .statusCode(200).body("submitPath", Matchers.equalTo("/user/tickets")) + RestAssured.given().get("/api/user/tickets/bootstrap").then().statusCode(200) + .body("submitPath", Matchers.equalTo("/user/tickets")) .body("defaultAffectsVersion.name", Matchers.equalTo("1.0.0")) .body("categories.name", Matchers.hasItem("Question")); User creatingUser = User.find("email", "user@mnemosyne-systems.ai").firstResult(); @@ -95,10 +99,9 @@ void userCanAccessUserTicketsMenu() { .find("entitlement = ?1 order by date asc, id asc", userCreateEntitlement.entitlement).firstResult(); Assertions.assertNotNull(userCreateVersion); String userCreateMessage = "User create redirect coverage"; - String userCreateRedirect = RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("status", "Open").multiPart("title", "User create redirect title") - .multiPart("message", userCreateMessage).multiPart("companyId", companyId) - .multiPart("companyEntitlementId", userCreateEntitlement.id) + String userCreateRedirect = RestAssured.given().redirects().follow(false).multiPart("status", "Open") + .multiPart("title", "User create redirect title").multiPart("message", userCreateMessage) + .multiPart("companyId", companyId).multiPart("companyEntitlementId", userCreateEntitlement.id) .multiPart("categoryId", Category.findDefault().id).multiPart("affectsVersionId", userCreateVersion.id) .post("/user/tickets").then().statusCode(303).extract().header("Location"); Message createdUserMessage = Message.find("body", userCreateMessage).firstResult(); @@ -112,54 +115,48 @@ void userCanAccessUserTicketsMenu() { Long ticketId = userTicket == null ? null : userTicket.id; User supportUser = User.find("email", "support1@mnemosyne-systems.ai").firstResult(); User tamUser = User.find("email", "tam1@mnemosyne-systems.ai").firstResult(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/user/tickets/" + ticketId).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/user/tickets/" + ticketId).then().statusCode(303) .header("Location", Matchers.endsWith("/user/tickets/" + ticketId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets/" + ticketId).then() - .statusCode(200).body("name", Matchers.equalTo(userTicketName)) + RestAssured.given().get("/api/user/tickets/" + ticketId).then().statusCode(200) + .body("name", Matchers.equalTo(userTicketName)) .body("title", Matchers.equalTo(userTicket.displayTitle())) .body("supportUsers.username", Matchers.hasItem("support1")) .body("secondaryUsersLabel", Matchers.equalTo("TAM")).body("editableStatus", Matchers.equalTo(false)) .body("editableResolvedVersion", Matchers.equalTo(false)) .body("exportPath", Matchers.equalTo("/tickets/export/" + ticketId)); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/user/companies/" + companyId).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/user/companies/" + companyId).then().statusCode(303) .header("Location", Matchers.endsWith("/user/companies/" + companyId)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/companies/" + companyId).then() - .statusCode(200).body("id", Matchers.equalTo(companyId.intValue())) - .body("users.username", Matchers.hasItem("user")) + RestAssured.given().get("/api/user/companies/" + companyId).then().statusCode(200) + .body("id", Matchers.equalTo(companyId.intValue())).body("users.username", Matchers.hasItem("user")) .body("superusers.username", Matchers.hasItem("superuser1")).body("tamUsers", Matchers.empty()); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie).get("/tickets/" + ticketId) - .then().statusCode(303).header("Location", Matchers.endsWith("/user/tickets/" + ticketId)); + RestAssured.given().redirects().follow(false).get("/tickets/" + ticketId).then().statusCode(303) + .header("Location", Matchers.endsWith("/user/tickets/" + ticketId)); User superuser = User.find("email", "superuser1@mnemosyne-systems.ai").firstResult(); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/user/superuser-users/" + superuser.id).then().statusCode(303) - .header("Location", Matchers.endsWith("/user/superuser-users/" + superuser.id)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/superuser-users/" + superuser.id) - .then().statusCode(200).body("typeLabel", Matchers.equalTo("Superuser")) - .body("username", Matchers.equalTo("superuser1")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/user/support-users/" + supportUser.id).then().statusCode(303) - .header("Location", Matchers.endsWith("/user/support-users/" + supportUser.id)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/support-users/" + supportUser.id) - .then().statusCode(200).body("typeLabel", Matchers.equalTo("Support")) - .body("username", Matchers.equalTo("support1")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/user/tam-users/" + tamUser.id).then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/user/superuser-users/" + superuser.id).then() + .statusCode(303).header("Location", Matchers.endsWith("/user/superuser-users/" + superuser.id)); + RestAssured.given().get("/api/user/superuser-users/" + superuser.id).then().statusCode(200) + .body("typeLabel", Matchers.equalTo("Superuser")).body("username", Matchers.equalTo("superuser1")); + RestAssured.given().redirects().follow(false).get("/user/support-users/" + supportUser.id).then() + .statusCode(303).header("Location", Matchers.endsWith("/user/support-users/" + supportUser.id)); + RestAssured.given().get("/api/user/support-users/" + supportUser.id).then().statusCode(200) + .body("typeLabel", Matchers.equalTo("Support")).body("username", Matchers.equalTo("support1")); + RestAssured.given().redirects().follow(false).get("/user/tam-users/" + tamUser.id).then().statusCode(303) .header("Location", Matchers.endsWith("/user/tam-users/" + tamUser.id)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tam-users/" + tamUser.id).then() - .statusCode(200).body("typeLabel", Matchers.equalTo("TAM")).body("username", Matchers.equalTo("tam1")); + RestAssured.given().get("/api/user/tam-users/" + tamUser.id).then().statusCode(200) + .body("typeLabel", Matchers.equalTo("TAM")).body("username", Matchers.equalTo("tam1")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .get("/user/tickets/" + ticketId + "/edit").then().statusCode(303) + RestAssured.given().redirects().follow(false).get("/user/tickets/" + ticketId + "/edit").then().statusCode(303) .header("Location", Matchers.endsWith("/user/tickets/" + ticketId)); } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void userReplyUsesLatestPendingMessageForTicketSlaColor() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("User SLA Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); @@ -169,56 +166,59 @@ void userReplyUsesLatestPendingMessageForTicketSlaColor() { ensureTimedMessage(ticket, "Initial requester message", "user@mnemosyne-systems.ai", java.time.LocalDateTime.now().minusMinutes(10)); - String cookie = login("user", "user"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets").then().statusCode(200) + RestAssured.given().get("/api/user/tickets").then().statusCode(200) .body("items.find { it.id == " + ticket.id + " }.slaColor", Matchers.equalTo("Red")); - RestAssured.given().redirects().follow(false).cookie(AuthHelper.AUTH_COOKIE, cookie) - .multiPart("body", "Follow-up from requester").post("/user/tickets/" + ticket.id + "/messages").then() - .statusCode(303); + RestAssured.given().redirects().follow(false).multiPart("body", "Follow-up from requester") + .post("/user/tickets/" + ticket.id + "/messages").then().statusCode(303); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets").then().statusCode(200) + RestAssured.given().get("/api/user/tickets").then().statusCode(200) .body("items.find { it.id == " + ticket.id + " }.slaColor", Matchers.equalTo("White")); ensureTimedMessage(ticket, "Support follow-up", "support1@mnemosyne-systems.ai", java.time.LocalDateTime.now().minusMinutes(1)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/user/tickets").then().statusCode(200) + RestAssured.given().get("/api/user/tickets").then().statusCode(200) .body("items.find { it.id == " + ticket.id + " }.slaColor", Matchers.equalTo("White")); } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void userTicketSearchMatchesVisibleMessagesOnly() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); Long companyId = ensureCompany("User Search Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai"); Ticket visibleTicket = ensureTicket(companyId); String visibleBody = "user-visible-search-" + System.nanoTime(); ensureMessageWithBody(visibleTicket, visibleBody); - String cookie = login("user", "user"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("q", visibleBody).get("/api/user/tickets") - .then().statusCode(200).body("searchTerm", Matchers.equalTo(visibleBody)) + RestAssured.given().queryParam("q", visibleBody).get("/api/user/tickets").then().statusCode(200) + .body("searchTerm", Matchers.equalTo(visibleBody)) .body("items.name", Matchers.hasItem(visibleTicket.name)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).queryParam("q", "user-search-no-match") - .get("/api/user/tickets").then().statusCode(200).body("items.size()", Matchers.equalTo(0)); + RestAssured.given().queryParam("q", "user-search-no-match").get("/api/user/tickets").then().statusCode(200) + .body("items.size()", Matchers.equalTo(0)); } - @Test - void reactLoginRedirectsUsersToUserTickets() { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - - RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("username", "user") - .formParam("password", "user").post("/login").then().statusCode(303) - .header("Location", Matchers.equalTo("/user/tickets")); - } + // @Test + // void reactLoginRedirectsUsersToUserTickets() { + // ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); + // + // RestAssured.given().redirects().follow(false).contentType(ContentType.URLENC).formParam("username", "user") + // .formParam("password", "user").post("/login").then().statusCode(303) + // .header("Location", Matchers.equalTo("/user/tickets")); + // } @Test + @TestSecurity(user = "user", roles = "user") + @JwtSecurity(claims = { @Claim(key = "email", value = "user@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user") }) void userTicketPdfExportIncludesAttachmentTitlesAndContent() throws Exception { - ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER, "user"); - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM, "tam1"); + ensureUser("user", "user@mnemosyne-systems.ai", User.TYPE_USER); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); + ensureUser("tam1", "tam1@mnemosyne-systems.ai", User.TYPE_TAM); ensureDefaultCategories(); Long companyId = ensureCompany("PDF Export Co"); ensureCompanyUsers(companyId, "user@mnemosyne-systems.ai", "tam1@mnemosyne-systems.ai"); @@ -229,9 +229,8 @@ void userTicketPdfExportIncludesAttachmentTitlesAndContent() throws Exception { ensureAttachment(message, "diagram.png", "image/png", smallPngBytes()); ensureAttachment(message, "embedded.pdf", "application/pdf", simplePdfBytes("Embedded PDF body")); - String cookie = login("user", "user"); - byte[] pdfBytes = RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/tickets/export/" + ticket.id) - .then().statusCode(200).contentType("application/pdf").extract().asByteArray(); + byte[] pdfBytes = RestAssured.given().get("/tickets/export/" + ticket.id).then().statusCode(200) + .contentType("application/pdf").extract().asByteArray(); try (PdfReader reader = new PdfReader(pdfBytes)) { PdfTextExtractor extractor = new PdfTextExtractor(reader); @@ -276,34 +275,38 @@ private byte[] smallPngBytes() throws IOException { } @Test + @TestSecurity(user = "support1", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "support1@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "support1") }) void reactArticlesApiReturnsArticleListAndDetail() { - ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT, "support1"); - String cookie = login("support1", "support1"); + ensureUser("support1", "support1@mnemosyne-systems.ai", User.TYPE_SUPPORT); ensureArticle("Zulu Runbook", "ops,prod", "## Zulu article\n\nOperational notes"); Article article = ensureArticle("Alpha Runbook", "ops,prod", "## Seed article\n\nOperational notes"); Attachment attachment = ensureArticleAttachment(article, "runbook.txt", "Attachment body"); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/articles").then().statusCode(200) - .body("canCreate", Matchers.equalTo(true)).body("createPath", Matchers.equalTo("/articles/new")) + RestAssured.given().get("/api/articles").then().statusCode(200).body("canCreate", Matchers.equalTo(true)) + .body("createPath", Matchers.equalTo("/articles/new")) .body("items.title", Matchers.hasItems("Alpha Runbook", "Zulu Runbook")); - List titles = RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/articles").then() - .statusCode(200).extract().jsonPath().getList("items.title"); + List titles = RestAssured.given().get("/api/articles").then().statusCode(200).extract().jsonPath() + .getList("items.title"); Assertions.assertTrue(titles.indexOf("Alpha Runbook") < titles.indexOf("Zulu Runbook")); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).get("/api/articles/" + article.id).then() - .statusCode(200).body("title", Matchers.equalTo("Alpha Runbook")) - .body("tags", Matchers.equalTo("ops,prod")).body("body", Matchers.containsString("Seed article")) - .body("canEdit", Matchers.equalTo(true)).body("canDelete", Matchers.equalTo(false)) + RestAssured.given().get("/api/articles/" + article.id).then().statusCode(200) + .body("title", Matchers.equalTo("Alpha Runbook")).body("tags", Matchers.equalTo("ops,prod")) + .body("body", Matchers.containsString("Seed article")).body("canEdit", Matchers.equalTo(true)) + .body("canDelete", Matchers.equalTo(false)) .body("editPath", Matchers.equalTo("/articles/" + article.id + "/edit")) .body("attachments.name", Matchers.hasItem("runbook.txt")) .body("attachments.downloadPath", Matchers.hasItem("/attachments/" + attachment.id + "/data")); } @Test + @TestSecurity(user = "user-json", roles = "support") + @JwtSecurity(claims = { @Claim(key = "email", value = "user-json@mnemosyne-systems.ai"), + @Claim(key = "sub", value = "user-json") }) void reactUserMutationsReturnJsonRedirects() { - ensureUser("user-json", "user-json@mnemosyne-systems.ai", User.TYPE_USER, "user-json"); + ensureUser("user-json", "user-json@mnemosyne-systems.ai", User.TYPE_USER); ensureDefaultCategories(); - String cookie = login("user-json", "user-json"); Long companyId = ensureCompany("User Json Co"); ensureCompanyUsers(companyId, "user-json@mnemosyne-systems.ai"); Entitlement entitlement = ensureEntitlement("User Json Entitlement", "User json detail"); @@ -312,8 +315,7 @@ void reactUserMutationsReturnJsonRedirects() { Assertions.assertNotNull(entry); Assertions.assertNotNull(version); - String redirectTo = RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie) - .header("X-Billetsys-Client", "react").multiPart("status", "Open") + String redirectTo = RestAssured.given().header("X-Billetsys-Client", "react").multiPart("status", "Open") .multiPart("title", "User json redirect title").multiPart("message", "User json redirect create") .multiPart("companyId", companyId).multiPart("companyEntitlementId", entry.id) .multiPart("categoryId", Category.findDefault().id).multiPart("affectsVersionId", version.id) @@ -321,10 +323,10 @@ void reactUserMutationsReturnJsonRedirects() { .body("redirectTo", Matchers.matchesPattern("/user/tickets/\\d+")).extract().path("redirectTo"); Long createdTicketId = Long.valueOf(redirectTo.substring(redirectTo.lastIndexOf('/') + 1)); - RestAssured.given().cookie(AuthHelper.AUTH_COOKIE, cookie).header("X-Billetsys-Client", "react") - .contentType(ContentType.URLENC).formParam("title", "User json redirect title") - .formParam("affectsVersionId", version.id).post("/user/tickets/" + createdTicketId).then() - .statusCode(200).body("redirectTo", Matchers.equalTo("/user/tickets/" + createdTicketId)); + RestAssured.given().header("X-Billetsys-Client", "react").contentType(ContentType.URLENC) + .formParam("title", "User json redirect title").formParam("affectsVersionId", version.id) + .post("/user/tickets/" + createdTicketId).then().statusCode(200) + .body("redirectTo", Matchers.equalTo("/user/tickets/" + createdTicketId)); } } diff --git a/src/backend/test/resources/application.properties b/src/backend/test/resources/application.properties index c54d82e2..4adc71e3 100644 --- a/src/backend/test/resources/application.properties +++ b/src/backend/test/resources/application.properties @@ -14,3 +14,19 @@ app.public-base-url=http://localhost:8081 cap.api.endpoint= cap.siteverify.url= cap.secret.key= + +quarkus.oidc.enabled=false +quarkus.oidc.tenant-enabled=false + +quarkus.test.security.enable=true + +quarkus.http.auth.permission.public.paths=/api/public/*,/api/app/session,/api/cap/*,/health/*,/metrics +quarkus.http.auth.permission.public.policy=permit +quarkus.http.auth.permission.public.priority=1 + +quarkus.http.auth.permission.authenticated.paths=/api/*,/articles/* +quarkus.http.auth.permission.authenticated.policy=authenticated +quarkus.http.auth.permission.authenticated.priority=2 + +quarkus.log.category."io.quarkus.oidc".level=ERROR +quarkus.log.category."io.quarkus.security".level=ERROR \ No newline at end of file diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index e78f8603..e11b6c78 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -39,6 +39,7 @@ "highlight.js": "^11.11.1", "iconsax-react": "^0.0.8", "iso-country-currency": "^0.7.2", + "keycloak-js": "^26.2.4", "lexical": "^0.43.0", "lodash": "^4.18.1", "lucide-react": "^1.8.0", @@ -3622,9 +3623,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3638,9 +3636,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3654,9 +3649,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3670,9 +3662,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3686,9 +3675,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3702,9 +3688,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3718,9 +3701,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3734,9 +3714,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3750,9 +3727,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3766,9 +3740,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3782,9 +3753,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3798,9 +3766,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3814,9 +3779,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4120,9 +4082,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4139,9 +4098,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4158,9 +4114,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4177,9 +4130,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8628,6 +8578,15 @@ "node": ">=4.0" } }, + "node_modules/keycloak-js": { + "version": "26.2.4", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.4.tgz", + "integrity": "sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw==", + "license": "Apache-2.0", + "workspaces": [ + "test" + ] + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8831,9 +8790,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8854,9 +8810,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8877,9 +8830,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8900,9 +8850,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/frontend/package.json b/src/frontend/package.json index ab78286e..8aa27655 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -54,6 +54,7 @@ "highlight.js": "^11.11.1", "iconsax-react": "^0.0.8", "iso-country-currency": "^0.7.2", + "keycloak-js": "^26.2.4", "lexical": "^0.43.0", "lodash": "^4.18.1", "lucide-react": "^1.8.0", diff --git a/src/frontend/src/auth/AuthContext.ts b/src/frontend/src/auth/AuthContext.ts new file mode 100644 index 00000000..a3c42b86 --- /dev/null +++ b/src/frontend/src/auth/AuthContext.ts @@ -0,0 +1,22 @@ +/* + * Eclipse Public License - v 2.0 + * + * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + * PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + */ + +import { createContext } from "react"; +import keycloak from "./keycloak"; + +export interface AuthContextType { + keycloak: typeof keycloak; + authenticated: boolean; + initialized: boolean; +} + +export const AuthContext = createContext({ + keycloak, + authenticated: false, + initialized: false, +}); diff --git a/src/frontend/src/auth/AuthProvider.tsx b/src/frontend/src/auth/AuthProvider.tsx new file mode 100644 index 00000000..10825d67 --- /dev/null +++ b/src/frontend/src/auth/AuthProvider.tsx @@ -0,0 +1,147 @@ +/* + * Eclipse Public License - v 2.0 + * + * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + * PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + */ + +import React, { useEffect, useState } from "react"; +import { AuthContext } from "./AuthContext"; +import keycloak from "./keycloak"; + +let initPromise: Promise | null = null; + +const getInitPromise = () => { + if (!initPromise) { + initPromise = keycloak.init({ + onLoad: "check-sso", + checkLoginIframe: false, + }); + } + return initPromise; +}; + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const [authenticated, setAuthenticated] = useState(false); + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + let active = true; + let intervalId: ReturnType | undefined; + + const initKeycloak = async () => { + // Check parameters before Keycloak init clears them + const hash = window.location.hash || ""; + const search = window.location.search || ""; + if ( + (hash.includes("kc_action=UPDATE_PASSWORD") && + hash.includes("kc_action_status=success")) || + (search.includes("kc_action=UPDATE_PASSWORD") && + search.includes("kc_action_status=success")) + ) { + sessionStorage.setItem("password_update_success", "true"); + } + + try { + const isAuth = await getInitPromise(); + + if (!active) return; + + setAuthenticated(isAuth); + setInitialized(true); + + if (isAuth) { + // Token refresh loop every 15s + intervalId = setInterval(() => { + keycloak + .updateToken(30) + .then((refreshed) => { + if (refreshed) { + console.log("Token refreshed"); + } + }) + .catch(() => { + console.error("Failed to refresh token"); + keycloak.logout(); + }); + }, 15000); + } + } catch (err) { + console.error("Keycloak initialization failed:", err); + if (active) { + setInitialized(true); + } + } + }; + + initKeycloak(); + + return () => { + active = false; + if (intervalId) { + clearInterval(intervalId); + } + }; + }, []); + + // Globally patch fetch once Keycloak is configured + useEffect(() => { + const originalFetch = window.fetch; + window.fetch = async (input, init) => { + let requestInit = init || {}; + const url = + typeof input === "string" + ? input + : input instanceof Request + ? input.url + : ""; + + // Only attach Authorization header to local API endpoints (/api/*) + const isLocalApi = + url.startsWith("/api/") || + url.includes(window.location.origin + "/api/"); + + if (isLocalApi && keycloak.token) { + try { + await keycloak.updateToken(30); + } catch (err) { + console.error("Token refresh failed during fetch intercept", err); + } + + const headers = new Headers(requestInit.headers); + headers.set("Authorization", `Bearer ${keycloak.token}`); + requestInit = { + ...requestInit, + headers, + }; + } + + return originalFetch(input, requestInit); + }; + + return () => { + window.fetch = originalFetch; + }; + }, []); + + if (!initialized) { + return ( +
+
+
+

+ Initializing secure session... +

+
+
+ ); + } + + return ( + + {children} + + ); +}; +export default AuthProvider; diff --git a/src/frontend/src/auth/keycloak.ts b/src/frontend/src/auth/keycloak.ts new file mode 100644 index 00000000..482514b3 --- /dev/null +++ b/src/frontend/src/auth/keycloak.ts @@ -0,0 +1,17 @@ +/* + * Eclipse Public License - v 2.0 + * + * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + * PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + */ + +import Keycloak from "keycloak-js"; + +const keycloak = new Keycloak({ + url: import.meta.env.VITE_KEYCLOAK_URL || "http://localhost:8180", + realm: import.meta.env.VITE_KEYCLOAK_REALM || "billetsys", + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || "billetsys-frontend", +}); + +export default keycloak; diff --git a/src/frontend/src/auth/useAuth.ts b/src/frontend/src/auth/useAuth.ts new file mode 100644 index 00000000..bb84c39f --- /dev/null +++ b/src/frontend/src/auth/useAuth.ts @@ -0,0 +1,14 @@ +/* + * Eclipse Public License - v 2.0 + * + * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + * PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + */ + +import { useContext } from "react"; +import { AuthContext } from "./AuthContext"; + +export const useAuth = () => { + return useContext(AuthContext); +}; diff --git a/src/frontend/src/components/layout/AuthenticatedHeader.tsx b/src/frontend/src/components/layout/AuthenticatedHeader.tsx index 4da03c5d..b07d05be 100644 --- a/src/frontend/src/components/layout/AuthenticatedHeader.tsx +++ b/src/frontend/src/components/layout/AuthenticatedHeader.tsx @@ -15,6 +15,7 @@ import { installationCompanyName } from "../../utils/installationBranding"; import { formatInstallationClock, toQueryString } from "../../utils/formatting"; import { SmartLink, normalizeClientPath } from "../../utils/routing"; import { isEditableTarget } from "../../utils/keyboard"; +import { useAuth } from "../../auth/useAuth"; import { ticketCountsApiPath, ticketLabelForRole, @@ -60,6 +61,7 @@ export default function AuthenticatedHeader({ }: AuthenticatedHeaderProps) { const [now, setNow] = useState(() => new Date()); const [userMenuOpen, setUserMenuOpen] = useState(false); + const { keycloak } = useAuth(); const { darkModeToggleRef, toggleDarkMode } = useDarkModeToggle(); const location = useLocation(); const navigate = useNavigate(); @@ -519,9 +521,15 @@ export default function AuthenticatedHeader({ -
setUserMenuOpen(false)}> + diff --git a/src/frontend/src/components/layout/SessionInactivityManager.tsx b/src/frontend/src/components/layout/SessionInactivityManager.tsx index 9d23ac3d..617c1fe7 100644 --- a/src/frontend/src/components/layout/SessionInactivityManager.tsx +++ b/src/frontend/src/components/layout/SessionInactivityManager.tsx @@ -8,6 +8,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { Session } from "../../types/app"; +import { useAuth } from "../../auth/useAuth"; import { Button } from "../ui/button"; import { AlertDialog, @@ -35,6 +36,7 @@ function formatRemainingTime(totalSeconds: number): string { export default function SessionInactivityManager({ session, }: SessionInactivityManagerProps) { + const { keycloak } = useAuth(); const timeoutSeconds = session?.inactivityTimeoutSeconds || DEFAULT_TIMEOUT_SECONDS; const warningSeconds = @@ -54,15 +56,8 @@ export default function SessionInactivityManager({ } timeoutTriggeredRef.current = true; setWarningOpen(false); - try { - await fetch("/logout", { - credentials: "same-origin", - cache: "no-store", - }); - } finally { - window.location.replace("/login"); - } - }, []); + keycloak.logout({ redirectUri: window.location.origin }); + }, [keycloak]); const keepAlive = useCallback( async (force = false) => { diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx index 37cd300f..e48fb173 100644 --- a/src/frontend/src/main.tsx +++ b/src/frontend/src/main.tsx @@ -12,6 +12,8 @@ import { BrowserRouter } from "react-router-dom"; import App from "./App"; import "./app.css"; +import { AuthProvider } from "./auth/AuthProvider"; + const rootElement = document.getElementById("root"); if (!rootElement) { @@ -29,7 +31,9 @@ if ( ReactDOM.createRoot(rootElement).render( - + + + , ); diff --git a/src/frontend/src/pages/LoginPage.tsx b/src/frontend/src/pages/LoginPage.tsx index 3bcc0b4b..ebc66be4 100644 --- a/src/frontend/src/pages/LoginPage.tsx +++ b/src/frontend/src/pages/LoginPage.tsx @@ -6,7 +6,7 @@ * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. */ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Link, Navigate, useLocation } from "react-router-dom"; import type { SessionPageProps } from "../types/app"; import { @@ -19,13 +19,21 @@ import { Button } from "../components/ui/button"; import { Field, FieldLabel } from "../components/ui/field"; import { Input } from "../components/ui/input"; import { EyeIcon, EyeOffIcon } from "lucide-react"; +import { useAuth } from "../auth/useAuth"; export default function LoginPage({ sessionState }: SessionPageProps) { + const { keycloak, authenticated } = useAuth(); const session = sessionState.data; const location = useLocation(); const error = new URLSearchParams(location.search).get("error"); const [showPassword, setShowPassword] = useState(false); + useEffect(() => { + if (!authenticated) { + keycloak.login(); + } + }, [authenticated, keycloak]); + const branding = { ...readCachedInstallationBranding(), ...pickInstallationBranding(session), @@ -37,6 +45,28 @@ export default function LoginPage({ sessionState }: SessionPageProps) { return ; } + if (!authenticated) { + return ( +
+
+

+ Redirecting to authentication service... +

+
+ ); + } + + if (sessionState.loading) { + return ( +
+
+

+ Loading user session... +

+
+ ); + } + return (
diff --git a/src/frontend/src/pages/PasswordPage.tsx b/src/frontend/src/pages/PasswordPage.tsx index aa17d813..e1219774 100644 --- a/src/frontend/src/pages/PasswordPage.tsx +++ b/src/frontend/src/pages/PasswordPage.tsx @@ -6,131 +6,32 @@ * OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. */ -import type { FormEvent } from "react"; -import { useEffect, useState } from "react"; -import { useLocation } from "react-router-dom"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; -import DataState from "../components/common/DataState"; -import PageHeader from "../components/layout/PageHeader"; -import type { SessionPageProps } from "../types/app"; -import { Button } from "../components/ui/button"; -import { Field, FieldLabel } from "../components/ui/field"; -import { Input } from "../components/ui/input"; +import { useAuth } from "../auth/useAuth"; -export default function PasswordPage({ sessionState }: SessionPageProps) { - const location = useLocation(); - - const [formState, setFormState] = useState({ - oldPassword: "", - newPassword: "", - confirmPassword: "", - }); - const [saveState, setSaveState] = useState({ - saving: false, - error: "", - saved: false, - }); - const routeError = new URLSearchParams(location.search).get("error") || ""; +export default function PasswordPage() { + const { keycloak } = useAuth(); + const navigate = useNavigate(); useEffect(() => { - if (routeError) { - toast.error(routeError); + if (sessionStorage.getItem("password_update_success") === "true") { + sessionStorage.removeItem("password_update_success"); + toast.success("Password updated successfully!"); + navigate("/profile", { replace: true }); + return; } - }, [routeError]); - const submit = async (event: FormEvent) => { - event.preventDefault(); - setSaveState({ saving: true, error: "", saved: false }); - try { - const response = await fetch("/api/profile/password", { - method: "POST", - credentials: "same-origin", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formState), - }); - if (!response.ok) { - throw new Error( - (await response.text()) || "Unable to update password.", - ); - } - setFormState({ oldPassword: "", newPassword: "", confirmPassword: "" }); - setSaveState({ saving: false, error: "", saved: true }); - toast.success("Password updated successfully."); - } catch (error: unknown) { - setSaveState({ - saving: false, - error: - error instanceof Error ? error.message : "Unable to update password.", - saved: false, - }); - } - }; + keycloak.login({ action: "UPDATE_PASSWORD" }); + }, [keycloak, navigate]); return ( -
- - -
- - Old password - - setFormState((current) => ({ - ...current, - oldPassword: event.target.value, - })) - } - required - /> - - - New password - - setFormState((current) => ({ - ...current, - newPassword: event.target.value, - })) - } - required - /> - - - Confirm password - - setFormState((current) => ({ - ...current, - confirmPassword: event.target.value, - })) - } - required - /> - - -
- -
-
-
-
+
+
+

+ Redirecting to secure password update service... +

+
); } diff --git a/src/frontend/src/routes/CoreRoutes.tsx b/src/frontend/src/routes/CoreRoutes.tsx index e2634a2f..f6ac33ae 100644 --- a/src/frontend/src/routes/CoreRoutes.tsx +++ b/src/frontend/src/routes/CoreRoutes.tsx @@ -1,4 +1,4 @@ -/* +/* * Eclipse Public License - v 2.0 * * THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE @@ -57,7 +57,7 @@ export function getCoreRoutes(sessionState: SessionState): AppRoute[] { }, { path: PATHS.profilePassword, - element: , + element: , requiresAuth: true, }, {