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
20 changes: 20 additions & 0 deletions third-party/keycloak-tf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ communicate with SW360 REST API with authentication from KeyCloak.
Scope to get read/write token: "openid email READ WRITE"
```

## Migrating legacy OAuth clients

If you are upgrading from an existing SW360 setup and want to import your
users' existing Liferay tokens from CouchDB into Keycloak seamlessly, you can
use the included `export_clients.py` helper script.

1. Install the necessary dependencies (e.g. `ibmcloudant`). Set your CouchDB
credentials into environment variables if required (`COUCHDB_URL`,
`COUCHDB_USER`, `COUCHDB_PASSWORD`).
2. Run `python export_clients.py > migrated_clients.tf` to extract the
configurations (including the exact `client_id` and `client_secret`
properties).
3. Replace the `sw360_read_clients` and `sw360_write_clients` maps in
`l-sw360-clients-list.tf` with the output of the script.
4. Run `tofu apply` to provision the identical clients directly inside Keycloak.
5. **Security Note:** Remove the `client_id` and `client_secret` fields from
`l-sw360-clients-list.tf` before committing your changes into your Git
repositories! Terraform is specifically configured to ignore subsequent
changes to these variables once created.

# Using KeyCloak to configure Frontend and Backend

After following the steps mentioned above and running `tofu apply`, your
Expand Down
139 changes: 139 additions & 0 deletions third-party/keycloak-tf/export_clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Copyright Siemens AG, 2026. Part of the SW360 Portal Project.
#
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0

"""
This script helps export OAuth clients from Liferay setup into new KeyCloak
setup using Terraform/OpenTofu.
The script expects tokens to have description in following format:
"<user_group>-<user_email>-<creator_email>-<creation_date>"
"""

import os
import re
import sys

# pylint: disable=import-error
from ibmcloudant.cloudant_v1 import CloudantV1
from ibm_cloud_sdk_core.authenticators import BasicAuthenticator


def fetch_documents():
"""Fetches all client documents from CouchDB."""
# User can override these via environment variables if needed
couch_url = os.environ.get("COUCHDB_URL", "http://localhost:5984")
couch_user = os.environ.get("COUCHDB_USER", "admin")
couch_pass = os.environ.get("COUCHDB_PASSWORD", "admin")

authenticator = BasicAuthenticator(couch_user, couch_pass)
service = CloudantV1(authenticator=authenticator)
service.set_service_url(couch_url)

db_name = "sw360oauthclients"

print(f"Connecting to CouchDB at {couch_url} to fetch database: "
f"{db_name}...", file=sys.stderr)

try:
response = service.post_all_docs(
db=db_name, include_docs=True
).get_result()
return response.get('rows', [])
except Exception as err: # pylint: disable=broad-exception-caught
print(f"Error fetching docs from CouchDB: {err}", file=sys.stderr)
sys.exit(1)


def process_documents(rows):
"""Parses raw CouchDB rows and returns deduplicated list of clients."""
clients_by_email = {}
pattern = re.compile(
r'^([A-Z0-9-]+)-([a-z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,})-'
r'([a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,})-(\d{4}-?\d{2}-?\d{2})'
r'(?:-.*)?$')

for row in rows:
doc = row.get('doc', {})
description = doc.get("description", "")
if not description:
continue

match = pattern.match(description)
if not match:
print(f"Skipping doc {doc.get('_id')} - unrecognized description "
f"format: {description}", file=sys.stderr)
continue

creation_date = match.group(4).replace("-", "")
user_email = match.group(2)

client_info = {
"user_email": user_email,
"creator_email": match.group(3),
"user_group": match.group(1),
"creation_date": creation_date,
"client_id": doc.get("client_id", ""),
"client_secret": doc.get("client_secret", ""),
"is_write": "WRITE" in doc.get("scope", [])
}

existing = clients_by_email.get(user_email)
if not existing or existing['creation_date'] < creation_date:
clients_by_email[user_email] = client_info

return list(clients_by_email.values())


def format_client(client):
"""Formats a client dictionary into Terraform HCL snippet."""
return f""" {{
user_email = "{client['user_email']}"
creator_email = "{client['creator_email']}"
user_group = "{client['user_group']}"
creation_date = "{client['creation_date']}"
client_id = "{client['client_id']}"
client_secret = "{client['client_secret']}"
}}"""


def main():
"""Main function to export clients."""
rows = fetch_documents()
clients = process_documents(rows)

# Sort deterministically by email for output
clients = sorted(clients, key=lambda x: x["user_email"])

read_clients = []
write_clients = []

for client_item in clients:
is_write = client_item.pop("is_write")
if is_write:
write_clients.append(client_item)
else:
read_clients.append(client_item)

# Generate the Terraform snippet
print("locals {")
print(" sw360_read_clients = [")
if read_clients:
print(",\n".join(format_client(client_item) for client_item in read_clients))
print(" ]")
print()
print(" sw360_write_clients = [")
if write_clients:
print(",\n".join(format_client(client_item) for client_item in write_clients))
print(" ]")
print()
print(" sw360_clients = concat(local.sw360_read_clients,"
" local.sw360_write_clients)")
print("}")


if __name__ == "__main__":
main()
15 changes: 13 additions & 2 deletions third-party/keycloak-tf/l-sw360-clients-list.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ The `sw360_write_clients` will have read-write access (ALL).
`creator_email`: User who authorized the token generation.
`user_group`: Group to which the user belongs.
`creation_date`: Date when the token was added.
`client_id`: If not exists, one is generated by the script. If you want to
override generated value, put it here.
`client_secret`: If not exists, one is generated by keycloak. If you want to
override generated value, put it here.

These information are used in documentation in the KeyCloak OpenID Client as:
"<user_group>-<user_email>-<creator_email>-<creation_date>"

Note: The `client_id` and `client_secret` are marked as
`lifecycle {ignore_changes}` in the respective resources. Thus it is advised to
run the `apply` with the values and then removing them before committing the
file changes to your git repo.
*/

locals {
Expand All @@ -24,15 +33,17 @@ locals {
user_email = "user1@sw360.org"
creator_email = "admin@sw360.org"
user_group = "DEPARTMENT1"
creation_date = "2025-05-07"
creation_date = "20250507"
client_id = "override-default"
client_secret = "override-default"
}
]
sw360_write_clients = [
{
user_email = "clearing.admin@sw360.org"
creator_email = "admin@sw360.org"
user_group = "DEPARTMENT2"
creation_date = "2025-05-07"
creation_date = "20250507"
}
]

Expand Down
1 change: 1 addition & 0 deletions third-party/keycloak-tf/local.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ frontend_base_url = "http://localhost:3000"
tenant = "azure-ad-tenant"
azure_client_id = "azure-idp-client-id"
azure_client_secret = "azure-idp-client-secret"
azure_idp_alias = "azure-sw360"
# Uncomment following to setup SMTP in KeyCloak for sending emails
# smtp_from = "admin@sw360.org"
# smtp_username = "my-smtp-user"
Expand Down
2 changes: 1 addition & 1 deletion third-party/keycloak-tf/r-identity-provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# 1. Identity provider
resource "keycloak_oidc_identity_provider" "entra_id" {
realm = keycloak_realm.sw360.id
alias = "azure-foss360"
alias = var.azure_idp_alias
display_name = "Login with EntraID"

authorization_url = "https://login.microsoftonline.com/${var.tenant}/oauth2/v2.0/authorize"
Expand Down
11 changes: 10 additions & 1 deletion third-party/keycloak-tf/r-user-clients.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ resource "keycloak_openid_client" "sw360_user_clients" {
for_each = { for client in local.sw360_clients : client.user_email => client }

realm_id = keycloak_realm.sw360.id
client_id = uuidv5("oid", each.key)
client_id = try(each.value.client_id, uuidv5("oid", each.key))

name = "${each.value.user_group}-${each.value.user_email}-${each.value.creator_email}-${each.value.creation_date}"
enabled = true

client_secret = try(each.value.client_secret, null)

access_type = "CONFIDENTIAL"
valid_redirect_uris = []
web_origins = []
Expand All @@ -25,6 +27,13 @@ resource "keycloak_openid_client" "sw360_user_clients" {
frontchannel_logout_enabled = false
consent_required = false
use_refresh_tokens = false

lifecycle {
ignore_changes = [
client_id,
client_secret
]
}
}

# Default scopes given to all clients
Expand Down
6 changes: 6 additions & 0 deletions third-party/keycloak-tf/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ variable "dashboard_base_url" {
type = string
default = null
}

variable "azure_idp_alias" {
description = "Alias for Azure EntraID Identity Provider"
type = string
default = "azure-sw360"
}
9 changes: 9 additions & 0 deletions third-party/thrift/install-thrift.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ set -ex
BASEDIR="${BASEDIR:-$(mktemp -d)}"
THRIFT_VERSION=${THRIFT_VERSION:-0.20.0}

installDeps() {
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
cmake \
curl \
flex \
bison
}

processThrift() {
VERSION=${THRIFT_VERSION}
CACHE_DIR="$BASEDIR/cache"
Expand Down Expand Up @@ -57,4 +65,5 @@ processThrift() {
DESTDIR="${DESTDIR:-$BASEDIR/dist/thrift-$VERSION}" make install
}

installDeps
processThrift
Loading