diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 81dcf707..4452a25a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,8 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "github-actions" + # Workflow files stored in the default location of `.github/workflows`. (You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.) + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c0f081a4..bbcfa5e4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,9 +4,6 @@ on: workflow_dispatch: push: branches: [ 'develop', 'master', 'releases/**' ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ 'develop', 'master', 'releases/**' ] schedule: - cron: '17 14 * * 2' @@ -19,4 +16,5 @@ jobs: # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support \ No newline at end of file + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + java_version: 21 \ No newline at end of file diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml index 1d4146ba..3cf893a1 100644 --- a/.github/workflows/maven-deploy.yml +++ b/.github/workflows/maven-deploy.yml @@ -32,6 +32,7 @@ jobs: with: environment: internal-publish release_type: snapshot + java_version: 21 secrets: username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} @@ -43,6 +44,7 @@ jobs: with: environment: ${{ inputs.environment }} release_type: ${{ inputs.release_type }} + java_version: 21 secrets: username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} diff --git a/.github/workflows/maven-test.yml b/.github/workflows/maven-test.yml index 6bdada9f..33932bf6 100644 --- a/.github/workflows/maven-test.yml +++ b/.github/workflows/maven-test.yml @@ -15,4 +15,6 @@ on: jobs: maven-tests: uses: wultra/wultra-infrastructure/.github/workflows/maven-test.yml@develop - secrets: inherit \ No newline at end of file + secrets: inherit + with: + java_version: 21 \ No newline at end of file diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index a47c0bfd..e1ae067d 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -7,6 +7,15 @@ on: - 'develop' - 'master' - 'releases/*' + inputs: + push_to_acr: + description: Push to ACR? + type: boolean + default: true + push_to_jfrog: + description: Push to JFrog? + type: boolean + default: false pull_request: branches: - 'develop' @@ -22,10 +31,10 @@ jobs: INTERNAL_USERNAME: ${{ secrets.JFROG_USERNAME }} INTERNAL_PASSWORD: ${{ secrets.JFROG_PASSWORD }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' server-id: jfrog-central server-username: INTERNAL_USERNAME @@ -41,25 +50,38 @@ jobs: run: | mvn -U -DuseInternalRepo=true --no-transfer-progress clean package - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: install: true - + - name: Log in to JFrog registry + if: inputs.push_to_jfrog == true + uses: docker/login-action@v3 + with: + registry: https://wultra.jfrog.io/ + username: ${{ vars.JFROG_CONTAINER_REGISTRY_USERNAME }} + password: ${{ secrets.JFROG_CONTAINER_REGISTRY_PASSWORD }} - name: Log in to Azure registry - if: ${{ github.event_name == 'workflow_dispatch' }} - uses: docker/login-action@v2 + if: inputs.push_to_acr == true + uses: docker/login-action@v3 with: registry: https://powerauth.azurecr.io/ - username: ${{ secrets.ACR_USERNAME }} + username: ${{ vars.ACR_USERNAME }} password: ${{ secrets.ACR_PASSWORD }} - name: Build and push container image to Azure registry - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: - push: ${{ github.event_name == 'workflow_dispatch' }} - platforms: linux/amd64 - tags: powerauth.azurecr.io/mobile-utility-server:${{ env.REVISION }}${{ env.TIMESTAMP }}-${{ github.sha }} + push: ${{ inputs.push_to_acr == true || inputs.push_to_jfrog == true }} + platforms: linux/amd64,linux/arm64 + tags: | + ${{ inputs.push_to_acr == true && format('powerauth.azurecr.io/mobile-utility-server:{0}{1}-{2}', env.REVISION, env.TIMESTAMP, github.sha) || '' }} + ${{ inputs.push_to_jfrog == true && format('wultra.jfrog.io/wultra-docker/mobile-utility-server:{0}{1}-{2}', env.REVISION, env.TIMESTAMP, github.sha) || '' }} file: ./deploy/dockerfile/runtime/Dockerfile context: . - + - run: echo '### 🚀 Published images' >> $GITHUB_STEP_SUMMARY + - if: inputs.push_to_acr == true + run: | + echo 'powerauth.azurecr.io/mobile-utility-server:${{ env.REVISION }}${{ env.TIMESTAMP }}-${{ github.sha }}' >> $GITHUB_STEP_SUMMARY + - if: inputs.push_to_jfrog == true + run: echo 'wultra.jfrog.io/wultra-docker/mobile-utility-server:${{ env.REVISION }}${{ env.TIMESTAMP }}-${{ github.sha }}' >> $GITHUB_STEP_SUMMARY diff --git a/deploy/dockerfile/runtime/docker-entrypoint.sh b/deploy/dockerfile/runtime/docker-entrypoint.sh index a54f6128..7dc41bee 100755 --- a/deploy/dockerfile/runtime/docker-entrypoint.sh +++ b/deploy/dockerfile/runtime/docker-entrypoint.sh @@ -1,10 +1,10 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash set -euo pipefail -liquibase --headless=true --log-level=INFO --changeLogFile=$LB_HOME/db/changelog/db.changelog-master.xml \ - --username=$MOBILE_UTILITY_SERVER_DATASOURCE_USERNAME \ - --password=$MOBILE_UTILITY_SERVER_DATASOURCE_PASSWORD \ - --url=$MOBILE_UTILITY_SERVER_DATASOURCE_URL \ +liquibase --headless=true --log-level=INFO --changeLogFile="${LB_HOME}/db/changelog/db.changelog-master.xml" \ + --username="${MOBILE_UTILITY_SERVER_DATASOURCE_USERNAME:-}" \ + --password="${MOBILE_UTILITY_SERVER_DATASOURCE_PASSWORD:-}" \ + --url="${MOBILE_UTILITY_SERVER_DATASOURCE_URL}" \ update java -Dserver.port=8000 -jar /mobile-utility-server.war diff --git a/docs/API-Admin.md b/docs/API-Admin.md new file mode 100644 index 00000000..5653c848 --- /dev/null +++ b/docs/API-Admin.md @@ -0,0 +1,959 @@ +# Administration Services + + + +The Administration Services of Mobile Utility Server provide functionality for managing applications, +certificates, versions, and text resources. + +## Possible Error Codes + +| HTTP Status Code | Error Code | Description | +|------------------|------------------------|---------------------------------------------------------------------------------------------------| +| `400` | `APP_EXCEPTION` | Indicates an error occurring during app operations like creating an already existing app. | +| `400` | `ERROR_REQUEST` | Request did not pass a structural validation (mandatory field is null, invalid field type, etc.). | +| `400` | `UNKNOWN_ERROR` | An error occurred when processing the request. Problems with request binding | +| `401` | `ERROR_AUTHENTICATION` | Unauthorized access attempt. This occurs when invalid credentials are provided. | +| `404` | `APP_NOT_FOUND` | Indicates that the app with the provided ID was not found. | +| `500` | `INTERNAL_ERROR` | An internal server error occurred, potentially due to misconfiguration. | + +## Services + + + +### Create Application + +Create a new application with specified name. + +#### Request + +##### Request Body + +```json +{ + "name": "mobile-app", + "displayName": "Mobile App" +} +``` + +| Attribute | Type | Description | +|---------------------------------------------------------------|----------|----------------------------------| +| `name`* | `String` | Name of the application. | +| `displayName`* | `String` | Display name of the application. | + +#### Response 200 + +```json +{ + "name": "Application Name", + "displayName": "Display name of the Application", + "publicKey": "pk1", + "domains": [ + { + "name": "name1", + "certificates": [ + { + "pem": "pem1", + "fingerprint": "fingerprint1", + "expires": 100 + } + ] + } + ] +} +``` + +| Attribute | Type | Description | +|----------------------------------------|------------|------------------------------------------------------| +| `name` | `String` | Name of the application. | +| `displayName` | `String` | Display name of the application. | +| `publicKey` | `String` | Public key of the application. | +| `domains` | `Object[]` | List of domain configurations for the application. | +| `domains[].name` | `String` | Name of the domain. | +| `domains[].certificates` | `Object[]` | List of certificates for the domain. | +| `domains[].certificates[].pem` | `String` | PEM-encoded certificate. | +| `domains[].certificates[].fingerprint` | `String` | Fingerprint of the certificate. | +| `domains[].certificates[].expires` | `Long` | Expiration time of the certificate in epoch seconds. | + +#### Response 400 + +Application creation failed due to invalid input or missing required fields. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_REQUEST", + "message": "Required fields are missing" + } +} +``` + +Application creation failed due to already used app name or error while generating cryptographic keys. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "APP_EXCEPTION", + "message": "Application with name already exists: XY / Error while generating cryptographic keys" + } +} +``` + +Possible Error states are: + +- `ERROR_REQUEST` - Request did not pass a structural validation (mandatory field is null, invalid field type, etc.). +- `APP_EXCEPTION` - Application with name already exists: XY or Error while generating cryptographic keys. + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### List Applications + +Retrieve a list of all registered applications. + +#### Response 200 + +```json +{ + "applications": [ + { + "name": "mobile-app-1", + "displayName": "Mobile App 1" + }, + { + "name": "mobile-app-2", + "displayName": "Mobile App 2" + } + ] +} +``` + +| Attribute | Type | Description | +|------------------------------|------------|----------------------------------| +| `applications` | `Object[]` | List of registered applications. | +| `applications[].name` | `String` | Name of the application. | +| `applications[].displayName` | `String` | Display name of the application. | + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Application Detail + +Retrieve detailed information about a specific application by its name. This endpoint provides complete details +including configuration settings. + +#### Request + +##### Path Params + +| Param | Type | Description | +|--------------------------------------------------------|----------|--------------------------------------------------| +| `name`* | `String` | Name of the application to retrieve details for. | + +#### Response 200 + +```json +{ + "name": "Application Name", + "displayName": "Display name of the Application", + "publicKey": "pk1", + "domains": [ + { + "name": "name1", + "certificates": [ + { + "pem": "pem1", + "fingerprint": "fingerprint1", + "expires": 100 + } + ] + } + ] +} +``` + +| Attribute | Type | Description | +|----------------------------------------|------------|------------------------------------------------------| +| `name` | `String` | Name of the application. | +| `displayName` | `String` | Display name of the application. | +| `publicKey` | `String` | Public key of the application. | +| `domains` | `Object[]` | List of domain configurations for the application. | +| `domains[].name` | `String` | Name of the domain. | +| `domains[].certificates` | `Object[]` | List of certificates for the domain. | +| `domains[].certificates[].pem` | `String` | PEM-encoded certificate. | +| `domains[].certificates[].fingerprint` | `String` | Fingerprint of the certificate. | +| `domains[].certificates[].expires` | `Long` | Expiration time of the certificate in epoch seconds. | + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Create Automatic Application Certificate + +Automatically generate and associate a certificate with a specified application. This endpoint handles the creation and +registration of a new certificate using server-defined parameters. + +#### Request + +##### Path Params + +| Param | Type | Description | +|--------------------------------------------------------|----------|--------------------------------------------------------------------| +| `name`* | `String` | Name of the application for which the certificate will be created. | + +##### Request Body + +```json +{ + "domain": "domain1" +} +``` + +| Attribute | Type | Description | +|----------------------------------------------------------|----------|-------------------------------------------------| +| `domain`* | `String` | The domain from which to fetch the certificate. | + +#### Response 200 + +```json +{ + "name": "Domain Name", + "pem": "pem1", + "fingerprint": "fingerprint1", + "expires": 100 +} +``` + +| Attribute | Type | Description | +|---------------------------------------------------------------|----------|---------------------------------------------| +| `name`* | `String` | Name of the domain. | +| `pem`* | `String` | PEM format of the certificate. | +| `fingerprint`* | `String` | Fingerprint of the certificate.. | +| `expires`* | `Long` | Timestamp when the certificate will expire. | + +#### Response 400 + +Certificate creation failed due to invalid input or missing required fields. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_REQUEST", + "message": "Required fields are missing" + } +} +``` + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + +#### Response 404 + +Failed to create the certificate because the requested app was not found. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "APP_NOT_FOUND", + "message": "App with a provided ID was not found." + } +} +``` + +#### Response 500 + +Error occurred during app execution. + +```json +{ + "timestamp": "TIMESTAMP", + "status": 500, + "error": "Internal Server Error", + "path": "/admin/apps/mobile-app22/certificates/auto" +} +``` + + + + +### Create PEM Application Certificate + +Manually add a PEM format certificate to a specified application. This endpoint accepts a PEM-encoded certificate and +associates it with the application. + +#### Request + +##### Path Params + +| Param | Type | Description | +|--------------------------------------------------------|----------|--------------------------------------------------------------------| +| `name`* | `String` | Name of the application to which the certificate will be attached. | + +##### Request Body + +```json +{ + "pem": "pem1" +} +``` + +| Attribute | Type | Description | +|-------------------------------------------------------|----------|---------------------------------| +| `pem`* | `String` | PEM encoded certificate string. | + +#### Response 200 + +```json +{ + "name": "Domain Name", + "pem": "pem1", + "fingerprint": "fingerprint1", + "expires": 100 +} +``` + +| Attribute | Type | Description | +|---------------------------------------------------------------|----------|---------------------------------------------| +| `name`* | `String` | Name of the domain. | +| `pem`* | `String` | PEM format of the certificate. | +| `fingerprint`* | `String` | Fingerprint of the certificate.. | +| `expires`* | `Long` | Timestamp when the certificate will expire. | + +#### Response 400 + +Certificate creation failed due to invalid input or missing required fields. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_REQUEST", + "message": "Required fields are missing" + } +} +``` + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + +#### Response 404 + +Failed to create the certificate because the requested app was not found. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "APP_NOT_FOUND", + "message": "App with a provided ID was not found." + } +} +``` + +#### Response 500 + +Error occurred during app execution. + +```json +{ + "timestamp": "TIMESTAMP", + "status": 500, + "error": "Internal Server Error", + "path": "/admin/apps/mobile-app22/certificates/auto" +} +``` + + + + + +### Delete Certificate + +Delete a certificate associated with a specific application based on domain and fingerprint criteria. + +#### Request + +##### Path Params + +| Param | Type | Description | +|--------------------------------------------------------|----------|------------------------------------------------------------------| +| `name`* | `String` | Name of the application from which certificates will be deleted. | + +##### Query Parameters + +| Parameter | Type | Description | +|---------------------------------------------------------------|----------|---------------------------------------------------------------------| +| `domain`* | `String` | Domain associated with the certificate. | +| `fingerprint`* | `String` | Fingerprint of the certificate to specifically target for deletion. | + +#### Response 200 + +```json +{ + "status": "OK" +} +``` + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Delete Domain + +Delete a domain associated with a specific application. + +#### Request + +##### Path Params + +| Param | Type | Description | +|--------------------------------------------------------|----------|----------------------------------------------------------------| +| `name`* | `String` | Name of the application from which the domain will be deleted. | + +##### Query Parameters + +| Parameter | Type | Description | +|----------------------------------------------------------|----------|--------------------------------------------| +| `domain`* | `String` | Domain to be deleted from the application. | + +#### Response 200 + +Indicates that the domain was successfully deleted. + +```json +{ + "status": "OK" +} +``` + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Delete Expired Certificates + +Remove all expired certificates from the system. This endpoint provides a cleanup mechanism for old or no longer valid +certificates. + +#### Response 200 + +Indicates that all expired certificates were successfully removed. + +```json +{ + "status": "OK" +} +``` + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### List Application Versions + +Retrieve a list of all versions for a specific application. This endpoint provides details about each version, including +version number and status. + +#### Request + +##### Path Parameters + +| Param | Type | Description | +|--------------------------------------------------------|----------|--------------------------------------------------------------| +| `name`* | `String` | Name of the application for which versions are being listed. | + +#### Response 200 + +```json +{ + "applicationVersions": [ + { + "id": 1, + "platform": "ANDROID", + "majorOsVersion": 12, + "suggestedVersion": "3.1.2", + "requiredVersion": "3.1.2", + "messageKey": "update_required" + } + ] +} +``` + +| Attribute | Type | Description | +|------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------| +| `applicationVersions` | `Object[]` | List of app versions. | +| `applicationVersions[].id`* | `Long` | Unique identifier of the application version. | +| `applicationVersions[].platform`* | `Enum` | Platform of the application (e.g., ANDROID, IOS). | +| `applicationVersions[].majorOsVersion` | `Integer` | Major OS version for the application, may be `null` to match all. | +| `applicationVersions[].suggestedVersion` | `String` | Suggested version of the application in SemVer 2.0 format. | +| `applicationVersions[].requiredVersion` | `String` | Required version of the application in SemVer 2.0 format. | +| `applicationVersions[].messageKey`* | `String` | Key for the message related to the version (e.g., for localization purposes). | + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Application Version Detail + +Retrieve detailed information about a specific version of an application. + +#### Request + +##### Path Parameters + +| Param | Type | Description | +|--------------------------------------------------------|----------|----------------------------------------------------| +| `name`* | `String` | Name of the application. | +| `id` * | `Long` | Identifier of the version to retrieve details for. | + +#### Response 200 + +```json + { + "id": 1, + "platform": "ANDROID", + "majorOsVersion": 12, + "suggestedVersion": "3.1.2", + "requiredVersion": "3.1.2", + "messageKey": "update_required" +} +``` + +| Attribute | Type | Description | +|--------------------------------------------------------------|-----------|-------------------------------------------------------------------------------| +| `id`* | `Long` | Unique identifier of the application version. | +| `platform`* | `Enum` | Platform of the application (e.g., ANDROID, IOS). | +| `majorOsVersion` | `Integer` | Major OS version for the application, may be `null` to match all. | +| `suggestedVersion` | `String` | Suggested version of the application in SemVer 2.0 format. | +| `requiredVersion` | `String` | Required version of the application in SemVer 2.0 format. | +| `messageKey`* | `String` | Key for the message related to the version (e.g., for localization purposes). | + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Create Application Version + +Add a new version to a specific application. + +#### Request + +##### Path Parameters + +| Param | Type | Description | +|--------------------------------------------------------|----------|------------------------------------------------| +| `name`* | `String` | Name of the application to add the version to. | + +##### Request Body + +```json +{ + "platform": "ANDROID", + "majorOsVersion": 12, + "suggestedVersion": "3.1.2", + "requiredVersion": "3.1.2", + "messageKey": "update_required" +} + +``` + +| Attribute | Type | Description | +|--------------------------------------------------------------------|-----------|-------------------------------------------------------------------------------| +| `platform`* | `Enum` | Platform of the application (e.g., ANDROID, IOS). | +| `majorOsVersion` | `Integer` | Major OS version for the application, may be `null` to match all. | +| `suggestedVersion`* | `String` | Suggested version of the application in SemVer 2.0 format. | +| `requiredVersion`* | `String` | Required version of the application in SemVer 2.0 format. | +| `messageKey` | `String` | Key for the message related to the version (e.g., for localization purposes). | + +#### Response 200 + +```json + { + "id": 1, + "platform": "ANDROID", + "majorOsVersion": 12, + "suggestedVersion": "3.1.2", + "requiredVersion": "3.1.2", + "messageKey": "update_required" +} +``` + +| Attribute | Type | Description | +|--------------------------------------------------------------|-----------|-------------------------------------------------------------------------------| +| `id`* | `Long` | Unique identifier of the application version. | +| `platform`* | `Enum` | Platform of the application (e.g., ANDROID, IOS). | +| `majorOsVersion` | `Integer` | Major OS version for the application, may be `null` to match all. | +| `suggestedVersion` | `String` | Suggested version of the application in SemVer 2.0 format. | +| `requiredVersion` | `String` | Required version of the application in SemVer 2.0 format. | +| `messageKey`* | `String` | Key for the message related to the version (e.g., for localization purposes). | + +#### Response 400 + +Certificate creation failed due to invalid input or missing required fields. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_REQUEST", + "message": "Required fields are missing" + } +} +``` + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Delete Application Version + +Remove a specific version from an application. This action is irreversible and should be used with caution to ensure +that no critical information is lost. + +#### Request + +##### Path Parameters + +| Param | Type | Description | +|--------------------------------------------------------|----------|--------------------------------------| +| `name`* | `String` | Name of the application. | +| `id`* | `String` | Identifier of the version to delete. | + +#### Response 200 + +Indicates that the version was successfully deleted. + +```json +{ + "status": "OK" +} +``` + + + + + +### List Texts + +Retrieve a list of all text entries managed within the system. + +#### Response 200 + +```json +{ + "texts": [ + { + "messageKey": "welcome_message", + "language": "en", + "text": "Welcome to our application!" + }, + { + "messageKey": "farewell_message", + "language": "es", + "text": "Gracias por visitar nuestra aplicación." + } + ] +} +``` + +| Attribute | Type | Description | +|----------------------------------------------------------------------|------------|-------------------------------------| +| `texts`* | `Object[]` | List of texts. | +| `texts[].messageKey`* | `String` | Unique key identifier for the text. | +| `texts[].language`* | `String` | ISO 639-1 two-letter language code. | +| `texts[].text`* | `String` | The content of the text. | + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Text Detail + +Retrieve detailed information about a specific text identified by its key and language. This endpoint provides the +content and the last update timestamp. + +#### Request + +##### Path Parameters + +| Param | Type | Description | +|------------------------------------------------------------|----------|----------------------------------| +| `key`* | `String` | The key identifier for the text. | +| `language`* | `String` | The language code for the text. | + +#### Response 200 + +```json +{ + "messageKey": "welcome_message", + "language": "en", + "text": "Welcome to our application!" +} +``` + +| Attribute | Type | Description | +|--------------------------------------------------------------|----------|-------------------------------------| +| `messageKey`* | `String` | Unique key identifier for the text. | +| `language`* | `String` | ISO 639-1 two-letter language code. | +| `text`* | `String` | The content of the text. | + +#### Response 401 + +Invalid username or password was provided while calling the service. + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_AUTHENTICATION", + "message": "Unauthorized" + } +} +``` + + + + + +### Create Text + +Add a new text entry to the system. + +#### Request + +##### Request Body + +```json +{ + "messageKey": "new_message", + "language": "fr", + "text": "Bienvenue dans notre application!" +} +``` + +| Attribute | Type | Description | +|--------------------------------------------------------------|----------|-------------------------------------| +| `messageKey`* | `String` | Unique key identifier for the text. | +| `language`* | `String` | ISO 639-1 two-letter language code. | +| `text`* | `String` | The content of the text. | + +#### Response 200 + +```json +{ + "messageKey": "new_message", + "language": "fr", + "text": "Bienvenue dans notre application!" +} +``` + +| Attribute | Type | Description | +|--------------------------------------------------------------|----------|-------------------------------------| +| `messageKey`* | `String` | Unique key identifier for the text. | +| `language`* | `String` | ISO 639-1 two-letter language code. | +| `text`* | `String` | The content of the text. | + +#### Response 400 + +```json +{ + "status": "ERROR", + "responseObject": { + "code": "ERROR_REQUEST", + "message": "Required fields are missing" + } +} +``` + + + + +### Delete Text + +Remove a specific text entry from the system identified by its key and language. + +#### Request + +##### Path Parameters + +| Param | Type | Description | +|------------------------------------------------------------|----------|----------------------------------| +| `key`* | `String` | The key identifier for the text. | +| `language`* | `String` | The language code for the text. | + +#### Response 200 + +```json +{ + "status": "OK" +} +``` + + diff --git a/docs/Database-Structure.md b/docs/Database-Structure.md index bf1aad44..747f4544 100644 --- a/docs/Database-Structure.md +++ b/docs/Database-Structure.md @@ -27,6 +27,11 @@ Contains information related to various mobile apps. | `display_name` | `VARCHAR(255)` | Display name of the application, a human readable value, such as `Wultra Demo App`. | | `sign_private_key` | `VARCHAR(255)` | Base64-encoded private key associated with the application. It is used for signing the data on the server side. | | `sign_public_key` | `VARCHAR(255)` | Base64-encoded public key associated with the application. It is used by the client applications when verifying data signed on the server side. | + +#### Sequence + +Sequence `mus_mobile_app_seq` responsible for mobile app autoincrements. + @@ -41,6 +46,15 @@ Contains information related to pinned domains. | `id` | `INTEGER` | Primary key for the table, automatically incremented value. | | `app_id` | `INTEGER` | Reference to related mobile app entity. | | `domain` | `VARCHAR(255)` | Host name of the domain, such as `mobile.wultra.com`. | + +#### Sequence + +Sequence `mus_mobile_domain_seq` responsible for mobile domain autoincrements. + +#### Indexes + +The tables are relatively small and as a result, do not require indexes. To marginally improve the lookup performance, you can create a foreign index to map the domain to mobile app. + @@ -57,6 +71,15 @@ Table with TLS/SSL certificate and fingerprints that should be pinned in the mob | `fingerprint` | `VARCHAR(255)` | Value of the certificate fingerprint. | | `expires` | `INTEGER` | Unix timestamp (seconds since Jan 1, 1970) of the certificate expiration. | | `mobile_domain_id` | `INTEGER` | Reference to related application domain in the `mus_mobile_domain` table. | + +#### Sequence + +Sequence `mus_certificate_seq` responsible for SSL certificates and fingerprints autoincrements. + +#### Indexes + +The tables are relatively small and as a result, do not require indexes. To marginally improve the lookup performance, you can create a foreign index for mapping the fingerprint to domain. + @@ -86,8 +109,12 @@ Table with users authorities. | `id` | `INTEGER` | Primary key for the table, automatically incremented value. | | `user_id` | `INTEGER` | Foreign key column referencing users in `mus_user` table. | | `authority` | `VARCHAR(255)` | Name of authority for the user prefixed with `ROLE_` (`ROLE_ADMIN`). | - +#### Indexes + +The tables are relatively small and as a result, do not require indexes. To marginally improve the lookup performance, you can create a foreign index to map the user authority to the user. + + ### Mobile Application Version @@ -105,6 +132,11 @@ Table to force or suggest update of mobile application version. | `suggested_version` | `VARCHAR(24)` | If the application version is lower, update is suggested. | | `required_version` | `VARCHAR(24)` | If the application version is lower, update is required. | | `message_key` | `VARCHAR(255)` | Together with language identifies row in `mus_localized_text` | + +#### Sequence + +Sequence `mus_mobile_app_version_seq` responsible for mobile application version autoincrements. + @@ -121,60 +153,3 @@ Table with localized texts. | `language` | `VARCHAR(2)` | Primary composite key for the table. ISO 639-1 two-letter language code. | | `text` | `TEXT` | Localized text. | - - -## Sequences - - -### Mobile App Sequence - -Sequence responsible for mobile app autoincrements. - - - - -### Mobile App Domain Sequence - -Sequence responsible for mobile domain autoincrements. - - - - -### SSL Certificate Sequence - -Sequence responsible for SSL certificates and fingerprints autoincrements. - - - - -### Mobile Application Version Sequence - -Sequence responsible for mobile application version autoincrements. - - - - -## Foreign Indexes - -The tables are relatively small and as a result, do not require indexes. To marginally improve the lookup performance, you can create the following foreign indexes. - - -### Foreign Index for SSL Fingerprint Lookup - -Foreign index for mapping the fingerprint to domain. - - - - -### Foreign Index for Domain Lookup - -Foreign index to map the domain to mobile app. - - - - -### Foreign Index for User Authority Lookup - -Foreign index to map the user authority to the user. - - diff --git a/docs/Readme.md b/docs/Readme.md index b32f7b1d..ca046536 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,16 +1,5 @@ # Mobile Utility Server - -## Table of Contents - -- [Overview](./Readme.md) -- [Deployment](./Deployment.md) -- [Database Structure](./Database-Structure.md) -- [Migration Instructions](./Migration-Instructions.md) -- [Configuration](./Configuration.md) -- [Public RESTful API](./Public-REST-API.md) - - The Mobile Utility Server by [Wultra](https://wultra.com) is a utility server with various features required by typical mobile apps. Currently, the feature set is relatively narrow: @@ -24,4 +13,4 @@ Mobile Utility Server is licensed using GNU AGPLv3 license. Please consult us at ## Contact -If you have any questions, please contact us at [hello@wultra.com](mailto:hello@wultra.com). +If you have any questions, please contact us at [hello@wultra.com](mailto:hello@wultra.com). \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index d2611bac..9d423f0f 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -1,11 +1,15 @@ -[Overview](./Readme.md) +**Introduction** -[Deployment](./Deployment.md) +- [Overview](./Readme.md) +- [Configuration](./Configuration.md) +- [Deployment](./Deployment.md) +- [Migration Instructions](./Migration-Instructions.md) -[Database Structure](./Database-Structure.md) +**API Reference** -[Migration Instructions](./Migration-Instructions.md) +- [Admin API](./API-Admin.md) +- [Public RESTful API](./Public-REST-API.md) -[Configuration](./Configuration.md) +**Reference** -[Public RESTful API](./Public-REST-API.md) +- [Database Structure](./Database-Structure.md) diff --git a/pom.xml b/pom.xml index e4990bde..a937f665 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ org.springframework.boot spring-boot-starter-parent - 3.2.3 + 3.3.2 com.wultra.app mobile-utility-server - 1.7.0 + 1.8.0 war Mobile Utility Server Utility server with various features suitable for mobile apps @@ -61,13 +61,13 @@ UTF-8 - 1.7.0 - 1.9.0 + 1.8.0 + 1.10.0 - 1.77 - 2.3.0 - 3.10.4 + 1.78.1 + 2.6.0 + 3.11.5 7.4 @@ -152,7 +152,7 @@ org.apache.maven maven-artifact - 3.9.6 + 3.9.8 @@ -245,7 +245,7 @@ io.gatling gatling-maven-plugin - 4.8.2 + 4.9.6 org.apache.maven.plugins diff --git a/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/ExceptionHandlingControllerAdvice.java b/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/ExceptionHandlingControllerAdvice.java index 9a133d0c..7b705a1e 100644 --- a/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/ExceptionHandlingControllerAdvice.java +++ b/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/ExceptionHandlingControllerAdvice.java @@ -17,12 +17,6 @@ */ package com.wultra.app.mobileutilityserver.rest.errorhandling; -import com.wultra.app.mobileutilityserver.rest.model.errors.ExtendedError; -import com.wultra.app.mobileutilityserver.rest.model.errors.Violation; -import io.getlime.core.rest.model.base.response.ErrorResponse; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.security.access.AccessDeniedException; @@ -37,6 +31,14 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import com.wultra.app.mobileutilityserver.rest.model.errors.ExtendedError; +import com.wultra.app.mobileutilityserver.rest.model.errors.Violation; +import io.getlime.core.rest.model.base.response.ErrorResponse; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; /** * Controller advice responsible for error handling. @@ -90,9 +92,9 @@ public class ExceptionHandlingControllerAdvice { @ExceptionHandler(InvalidChallengeHeaderException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public @ResponseBody ErrorResponse handleInvalidChallengeHeaderException(InvalidChallengeHeaderException ex) { - final String code = "INSUFFICIENT_CHALLENGE"; - final String message = "Request does not contain sufficiently strong challenge header, 16B is required at least."; - logger.error("Request does not contain sufficiently strong challenge header, 16B is required at least: {}", ex.getMessage()); + final String code = "INVALID_CHALLENGE"; + final String message = "Request does not contain correct challenge header, a random Base64 encoded challenge with 16B - 32B raw length is required."; + logger.error("Request does not contain correct challenge header, a random Base64 encoded challenge with 16B - 32B raw length is required: {}", ex.getMessage()); logger.debug("Exception detail: ", ex); return new ErrorResponse(code, message); } @@ -222,4 +224,18 @@ public class ExceptionHandlingControllerAdvice { logger.debug("Exception detail: ", e); return new io.getlime.core.rest.model.base.response.ErrorResponse("ERROR_AUTHENTICATION", e.getMessage()); } + + /** + * Exception handler for no resource found. + * + * @param e Exception. + * @return Response with error details. + */ + @ExceptionHandler(NoResourceFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public @ResponseBody ErrorResponse handleNoResourceFoundException(final NoResourceFoundException e) { + logger.warn("Error occurred when calling an API: {}", e.getMessage()); + logger.debug("Exception detail: ", e); + return new ErrorResponse("ERROR_NOT_FOUND", "Resource not found."); + } } diff --git a/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/InvalidChallengeHeaderException.java b/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/InvalidChallengeHeaderException.java index 59aca63f..8dce0151 100644 --- a/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/InvalidChallengeHeaderException.java +++ b/src/main/java/com/wultra/app/mobileutilityserver/rest/errorhandling/InvalidChallengeHeaderException.java @@ -19,7 +19,7 @@ package com.wultra.app.mobileutilityserver.rest.errorhandling; /** - * Exception thrown in the case challenge header is not present or is not sufficiently complex. + * Exception thrown in the case challenge header is not present or is not adequately complex. * * @author Petr Dvorak, petr@wultra.com */ diff --git a/src/main/java/com/wultra/app/mobileutilityserver/rest/http/HttpHeaders.java b/src/main/java/com/wultra/app/mobileutilityserver/rest/http/HttpHeaders.java index e7783df3..8fc5b330 100644 --- a/src/main/java/com/wultra/app/mobileutilityserver/rest/http/HttpHeaders.java +++ b/src/main/java/com/wultra/app/mobileutilityserver/rest/http/HttpHeaders.java @@ -17,22 +17,49 @@ */ package com.wultra.app.mobileutilityserver.rest.http; +import java.util.Base64; + +import org.apache.commons.lang3.StringUtils; + +import lombok.extern.slf4j.Slf4j; + /** * Class with constants for HTTP request / response headers. * * @author Petr Dvorak, petr@wultra.com */ +@Slf4j public class HttpHeaders { public static final int MIN_CHALLENGE_HEADER_LENGTH = 16; + public static final int MAX_CHALLENGE_HEADER_LENGTH = 32; public static final String REQUEST_CHALLENGE = "X-Cert-Pinning-Challenge"; public static final String RESPONSE_SIGNATURE = "X-Cert-Pinning-Signature"; + private HttpHeaders() { + throw new IllegalStateException("Should not be instantiated."); + } + public static boolean validChallengeHeader(String challengeHeader) { - return challengeHeader != null - && challengeHeader.length() >= HttpHeaders.MIN_CHALLENGE_HEADER_LENGTH - && !challengeHeader.isBlank(); + try { + if (StringUtils.isEmpty(challengeHeader)) { + logger.warn("Missing or blank challenge header: {}", challengeHeader); + return false; + } + final byte[] challengeBytes = Base64.getDecoder().decode(challengeHeader); + final int challengeLength = challengeBytes.length; + if (challengeLength >= MIN_CHALLENGE_HEADER_LENGTH && challengeLength <= MAX_CHALLENGE_HEADER_LENGTH) { + return true; + } else { + logger.warn("Invalid challenge size, must be between {} and {}, was: {}", MIN_CHALLENGE_HEADER_LENGTH, MAX_CHALLENGE_HEADER_LENGTH, challengeLength); + return false; + } + } catch (IllegalArgumentException ex) { + logger.warn("Invalid Base64 value received in the header: {}", challengeHeader); + logger.debug("Exception detail: {}", ex.getMessage(), ex); + return false; + } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 50a88e5d..d01f6d28 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -61,3 +61,6 @@ management.tracing.sampling.probability=1.0 #management.endpoints.web.exposure.include=health, prometheus #management.endpoint.prometheus.enabled=true #management.prometheus.metrics.export.enabled=true + +spring.autoconfigure.exclude=\ + org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration diff --git a/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index d9918a05..7de80dc1 100644 --- a/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -1,5 +1,5 @@ - + diff --git a/src/test/java/com/wultra/app/mobileutilityserver/rest/http/HttpHeadersTest.java b/src/test/java/com/wultra/app/mobileutilityserver/rest/http/HttpHeadersTest.java new file mode 100644 index 00000000..0555ab5b --- /dev/null +++ b/src/test/java/com/wultra/app/mobileutilityserver/rest/http/HttpHeadersTest.java @@ -0,0 +1,73 @@ +/* + * Wultra Mobile Utility Server + * Copyright (C) 2024 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.mobileutilityserver.rest.http; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; + +/** + * Test for {@link HttpHeaders}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +class HttpHeadersTest { + + @Test + void testValidChallengeHeader_success() { + final String input = Base64.getEncoder().encodeToString(new byte[20]); + + final boolean result = HttpHeaders.validChallengeHeader(input); + + assertTrue(result); + } + + @Test + void testValidChallengeHeader_empty() { + final boolean result = HttpHeaders.validChallengeHeader(""); + + assertFalse(result); + } + + @Test + void testValidChallengeHeader_tooShort() { + final String input = Base64.getEncoder().encodeToString(new byte[14]); + + final boolean result = HttpHeaders.validChallengeHeader(input); + + assertFalse(result); + } + + @Test + void testValidChallengeHeader_tooLong() { + final String input = Base64.getEncoder().encodeToString(new byte[33]); + + final boolean result = HttpHeaders.validChallengeHeader(input); + + assertFalse(result); + } + + @Test + void testValidChallengeHeader_notBase64() { + final boolean result = HttpHeaders.validChallengeHeader("&-+"); + + assertFalse(result); + } +}