Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
on:
push:
branches-ignore:
- main
pull_request:

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t protegeproject/webprotege-keycloak:test .
- name: Run entrypoint integration test
run: ./test-entrypoint.sh
66 changes: 66 additions & 0 deletions .github/workflows/notify-deploy-project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Notify WebProtege Deploy

on:
workflow_call:
inputs:
service:
required: true
type: string
version:
required: true
type: string
branch_var:
required: true
type: string
secrets:
PROTEGE_PROJECT_CLIENT_ID:
required: true
PROTEGE_PROJECT_CLIENT_SECRET:
required: true

jobs:
notify:
runs-on: ubuntu-latest

steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PROTEGE_PROJECT_CLIENT_ID }}
private-key: ${{ secrets.PROTEGE_PROJECT_CLIENT_SECRET }}
owner: protegeproject
repositories: webprotege-deploy

- name: Trigger workflow_call in webprotege-deploy
run: |
SERVICE="${{ inputs.service }}"
VERSION="${{ inputs.version }}"
BRANCH="${{ inputs.branch_var }}"
echo "Triggering webprotege-deploy with service=$SERVICE and version=$VERSION"

response=$(curl -s -w "%{http_code}" -X POST \
-H "Authorization: token ${{ steps.app-token.outputs.token }}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/protegeproject/webprotege-deploy/actions/workflows/update-compose.yml/dispatches \
-d '{
"ref": "'"$BRANCH"'",
"inputs": {
"service": "'"$SERVICE"'",
"version": "'"$VERSION"'",
"branch": "'"$BRANCH"'"
}
}')

http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')

echo "HTTP Status Code: $http_code"
echo "Response Body: $body"

if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then
echo "Error: API call failed with status code $http_code"
exit 1
else
echo "API call successful, status code: $http_code"
fi
106 changes: 90 additions & 16 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,120 @@ name: Release

on:
push:
tags:
- 'v*.*.*'
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.actor != 'protegeproject-bot[bot]' }}
outputs:
version: ${{ steps.release-outputs.outputs.version }}
service: ${{ steps.release-outputs.outputs.service }}

steps:
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_PASSWORD}}
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.PROTEGEPROJECT_BOT_APP_ID }}
private-key: ${{ secrets.PROTEGEPROJECT_BOT_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
- name: Set up Java
with:
token: ${{ steps.app-token.outputs.token }}
ref: ${{ github.head_ref }}
- name: Set up Maven Central Repository
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Extract version from tag
id: get-version
server-id: docker.io
server-username: DOCKER_USERNAME
server-password: DOCKER_PASSWORD

# Read the version from pom.xml and strip the -SNAPSHOT suffix to
# produce the release version. For example, 2.0.0-SNAPSHOT becomes
# 2.0.0. If there is no -SNAPSHOT suffix the version is used as-is.
- name: Determine release version
id: release-version
run: |
current_version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
echo "Current pom version: $current_version"
release_version=$(echo "$current_version" | sed -E 's/-SNAPSHOT$//')
echo "Release version: $release_version"
echo "release_version=$release_version" >> $GITHUB_OUTPUT

# Set pom.xml to the release version (without -SNAPSHOT), commit,
# and tag. This is the commit that the Docker image is built from.
- name: Set release version in pom.xml
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Set version in pom.xml files
find . -name 'pom.xml' -exec mvn versions:set \
-DnewVersion=${{ steps.release-version.outputs.release_version }} \
-DgenerateBackupPoms=false -f {} \;
- name: Commit and tag release
run: |
VERSION=${{ steps.get-version.outputs.version }}
find . -name 'pom.xml' -exec mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -f {} \;
- name: Build SPI plugin
run: mvn --batch-mode -pl spi clean package
- name: Build and push Docker image
run: mvn --batch-mode package install
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add .
# If the pom already had the release version (no -SNAPSHOT suffix),
# there is nothing to commit — just tag the current HEAD.
git diff --cached --quiet || \
git commit -m "Release ${{ steps.release-version.outputs.release_version }}"
git tag ${{ steps.release-version.outputs.release_version }}
git push origin HEAD:${GITHUB_REF##*/}
git push origin ${{ steps.release-version.outputs.release_version }}

- name: Build Docker image
run: mvn --batch-mode package
- name: Run entrypoint integration test
run: ./test-entrypoint.sh protegeproject/webprotege-keycloak:${{ steps.release-version.outputs.release_version }}
- name: Push Docker image
run: docker push protegeproject/webprotege-keycloak:${{ steps.release-version.outputs.release_version }}
- name: Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
tag_name: ${{ steps.release-version.outputs.release_version }}
generate_release_notes: true

# After the release, bump the patch version and add -SNAPSHOT to
# prepare the pom for the next development cycle. For example,
# after releasing 2.0.0 the pom is set to 2.0.1-SNAPSHOT.
- name: Prepare next development version
run: |
released=${{ steps.release-version.outputs.release_version }}
IFS='.' read -r -a parts <<< "$released"
parts[2]=$((parts[2] + 1))
next_snapshot="${parts[0]}.${parts[1]}.${parts[2]}-SNAPSHOT"
echo "Next development version: $next_snapshot"
find . -name 'pom.xml' -exec mvn versions:set \
-DnewVersion=$next_snapshot \
-DgenerateBackupPoms=false -f {} \;
git add .
git commit -m "Prepare next development version ($next_snapshot)"
git push origin HEAD:${GITHUB_REF##*/}

- name: Set outputs
id: release-outputs
run: |
echo "version=${{ steps.release-version.outputs.release_version }}" >> $GITHUB_OUTPUT
echo "service=webprotege-keycloak" >> $GITHUB_OUTPUT

notify-bump:
needs: build
uses: ./.github/workflows/notify-deploy-project.yaml
with:
version: ${{ needs.build.outputs.version }}
service: ${{ needs.build.outputs.service }}
branch_var: ${{vars.BUMP_WEBPROTEGE_BRANCH}}
secrets:
PROTEGE_PROJECT_CLIENT_ID: ${{ secrets.PROTEGE_PROJECT_CLIENT_ID }}
PROTEGE_PROJECT_CLIENT_SECRET: ${{ secrets.PROTEGE_PROJECT_CLIENT_SECRET }}

env:
DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Maven build output
spi/target/
*.versionsBackup

# JVM
hsperfdata_keycloak/
Expand Down
20 changes: 19 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
FROM alpine:3 AS tools
ARG TARGETARCH
RUN wget -O /usr/local/bin/jq \
"https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${TARGETARCH}" \
&& chmod +x /usr/local/bin/jq

FROM maven:3.9-eclipse-temurin-17 AS spi-builder
WORKDIR /build
COPY spi/pom.xml .
RUN mvn dependency:go-offline -B
COPY spi/src ./src
RUN mvn package -B -DskipTests

FROM keycloak/keycloak:26.1
COPY --from=tools /usr/local/bin/jq /usr/bin/jq
COPY ./webprotege /opt/keycloak/themes/webprotege
COPY ./spi/target/webprotege-credential-check-authenticator-*.jar /opt/keycloak/providers/
COPY --from=spi-builder /build/target/webprotege-credential-check-authenticator-*.jar /opt/keycloak/providers/
COPY ./webprotege.json /opt/keycloak/import/webprotege.json
COPY --chmod=755 ./entrypoint.sh /opt/keycloak/bin/entrypoint.sh

RUN /opt/keycloak/bin/kc.sh build

ENTRYPOINT ["/opt/keycloak/bin/entrypoint.sh"]
92 changes: 71 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,89 @@ Keycloak configuration for WebProtege. This repository contains:

## Prerequisites

- Java 17+
- Maven 3.8+
- Docker

## Building the Plugin
## Docker Build

The `Dockerfile` uses a multi-stage build to compile the authenticator plugin,
download tools, and package the theme, realm configuration, and startup
entrypoint into a custom Keycloak image. No local Java or Maven installation
is required.

The authenticator plugin must be built before deploying Keycloak:
To build locally:

```bash
cd spi
mvn clean package
docker build -t protegeproject/webprotege-keycloak:1.2.0 .
```

This produces `spi/target/webprotege-credential-check-authenticator-1.0.0.jar`.
## Startup Entrypoint

## Docker Build
The image includes a custom entrypoint script (`entrypoint.sh`) that wraps the
standard Keycloak startup. It starts Keycloak normally, waits for the realm
import to complete, then applies two configuration patches that cannot be
achieved through the realm import alone.

The `Dockerfile` packages the theme, plugin, and realm configuration into a custom Keycloak image:
### 1. Protocol Mapper Fix

```dockerfile
FROM keycloak/keycloak:26.1
COPY ./webprotege /opt/keycloak/themes/webprotege
COPY ./spi/target/webprotege-credential-check-authenticator-1.0.0.jar /opt/keycloak/providers/
COPY ./webprotege.json /opt/keycloak/import/webprotege.json
RUN /opt/keycloak/bin/kc.sh build
```
Keycloak's realm import mechanism has a known limitation: it silently drops the
`config` dictionary for certain protocol mapper types. The realm JSON defines a
`username` mapper in the `profile` client scope that maps the custom user
attribute `webprotege_username` to the `preferred_username` JWT claim. After
import, Keycloak reverts this mapper to its built-in default, which maps the
Keycloak username field instead.

To build locally:
This matters because WebProtege uses email addresses as Keycloak usernames, but
internal application lookups rely on the original MongoDB user ID stored in the
`webprotege_username` attribute. Without the fix, the backend receives email
addresses where it expects user IDs, breaking user resolution.

```bash
cd spi && mvn clean package && cd ..
docker build -t protegeproject/webprotege-keycloak:1.0.0 .
```
The entrypoint detects this condition on each startup and, if the mapper is in
the wrong state, deletes it and recreates it with the correct configuration.
On subsequent boots where the mapper is already correct, the fix is skipped.

### 2. Hostname-Based Client URI Patching

The realm JSON ships with a default hostname baked into the `webprotege`
client's redirect URIs, web origins, and base URL. Self-hosted deployments use
different hostnames. When the `SERVER_HOST` environment variable is set, the
entrypoint updates these values so that:

- Keycloak accepts OAuth redirects back to the correct host
- CORS headers include the correct origin
- The Keycloak login and account pages link back to the correct application URL
- The realm's OpenID Connect discovery document advertises the correct issuer

This makes the image portable — the same build works for local development,
staging, and production by setting `SERVER_HOST` in the deployment environment.

### Environment Variables

| Variable | Required | Default | Purpose |
|---|---|---|---|
| `KEYCLOAK_ADMIN` | Yes | `admin` | Admin username for kcadm authentication |
| `KEYCLOAK_ADMIN_PASSWORD` | Yes | `password` | Admin password for kcadm authentication |
| `KC_HTTP_RELATIVE_PATH` | Yes | *(none)* | Keycloak's HTTP relative path (e.g. `/keycloak`) |
| `SERVER_HOST` | No | *(none)* | Public hostname; when set, client URIs and the realm frontend URL are updated to match |

## Roles

The realm defines a single application-level role on the `webprotege`
client: **`SystemAdmin`**. This is the bootstrap admin role for new
installs — assigning it to a user grants full administrative access to
WebProtege (manage application settings, create projects, edit roles,
delete accounts, etc.).

See the [webprotege-deploy README](../webprotege-deploy/README.md#first-admin-bootstrap)
for step-by-step instructions on assigning the role to the first admin.

All other authorization in WebProtege is managed inside the application
itself via the authorization service's `RoleAssignment` collection and
the Application Settings UI. Keycloak roles are used only for the
bootstrap admin — this keeps identity (Keycloak) and authorization
(WebProtege) cleanly separated.

Automating the initial admin assignment on fresh installs is tracked in
[webprotege-authorization-service#36](https://github.com/protegeproject/webprotege-authorization-service/issues/36).

## Deployment

Expand Down
Loading
Loading