Files
theta-suite/migrate-ldap.sh

459 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# LDAP Migration Script for theta42
#
# Migrates an existing OpenLDAP server to the theta42 stack.
# Exports data from source, transforms as needed, imports into theta42.
#
# Usage:
# ./migrate-ldap.sh --source-host <ldap-uri> --source-bind-dn <dn> --source-bind-pass <pass> --target-domain <domain>
#
# Example:
# ./migrate-ldap.sh --source-host ldap://192.168.1.10:389 --source-bind-dn "cn=admin,dc=example,dc=com" --source-bind-pass "secret" --target-domain "example.com"
#
set -euo pipefail
cd "$(dirname "$0")"
# ── Defaults ──────────────────────────────────────────────────────────────────
SOURCE_HOST=""
SOURCE_BIND_DN=""
SOURCE_BIND_PASS=""
TARGET_DOMAIN=""
BASE_DN=""
EXPORT_DIR="./ldap-migration-$(date +%Y%m%d-%H%M%S)"
THETA_ENV_DIR="$(cd "$(dirname "$0")" && pwd)"
# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
info() { printf "${BLUE}[migrate]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[migrate]${NC} %s\n" "$*" >&2; }
error() { printf "${RED}[migrate]${NC} %s\n" "$*" >&2; }
success() { printf "${GREEN}[migrate]${NC} %s\n" "$*" >&2; }
die() { error "$*"; exit 1; }
# ── Argument parsing ──────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--source-host)
SOURCE_HOST="$2"
shift 2
;;
--source-bind-dn)
SOURCE_BIND_DN="$2"
shift 2
;;
--source-bind-pass)
SOURCE_BIND_PASS="$2"
shift 2
;;
--target-domain)
TARGET_DOMAIN="$2"
shift 2
;;
--export-dir)
EXPORT_DIR="$2"
shift 2
;;
--help|-h)
cat <<EOF
LDAP Migration Script for theta42
Usage: $0 --source-host <uri> --source-bind-dn <dn> --source-bind-pass <pass> --target-domain <domain>
Options:
--source-host Source LDAP URI (e.g., ldap://192.168.1.10:389 or ldaps://ldap.example.com:636)
--source-bind-dn Bind DN for source LDAP (e.g., cn=admin,dc=example,dc=com)
--source-bind-pass Bind password for source LDAP
--target-domain Target domain for theta42 (e.g., example.com)
--export-dir Directory for exports (default: ./ldap-migration-<timestamp>)
--help Show this help message
EOF
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
# ── Validation ────────────────────────────────────────────────────────────────
[[ -n "$SOURCE_HOST" ]] || die "Missing --source-host"
[[ -n "$SOURCE_BIND_DN" ]] || die "Missing --source-bind-dn"
[[ -n "$SOURCE_BIND_PASS" ]] || die "Missing --source-bind-pass"
[[ -n "$TARGET_DOMAIN" ]] || die "Missing --target-domain"
# Derive base DN from domain (e.g., example.com -> dc=example,dc=com)
BASE_DN="$(echo "$TARGET_DOMAIN" | sed 's/\./,dc=/g; s/^/dc=/')"
info "Migration configuration:"
info " Source host: $SOURCE_HOST"
info " Source bind DN: $SOURCE_BIND_DN"
info " Target domain: $TARGET_DOMAIN"
info " Target base DN: $BASE_DN"
info " Export dir: $EXPORT_DIR"
# ── Prerequisites ─────────────────────────────────────────────────────────────
command -v ldapsearch >/dev/null 2>&1 || die "ldapsearch not found. Install ldap-utils."
command -v slapcat >/dev/null 2>&1 || die "slapcat not found."
command -v docker >/dev/null 2>&1 || die "docker not found."
command -v docker-compose >/dev/null 2>&1 || command -v docker compose >/dev/null 2>&1 || die "docker compose not found."
if [[ -d "$EXPORT_DIR" ]]; then
warn "Export directory already exists: $EXPORT_DIR"
read -p "Overwrite? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
info "Aborted."
exit 1
fi
fi
mkdir -p "$EXPORT_DIR"
# ── Phase 1: Export from source LDAP ─────────────────────────────────────────
info "Phase 1: Exporting data from source LDAP..."
# Export each subtree
export_subtree() {
local base="$1"
local outfile="$2"
info " Exporting $base -> $outfile"
# Use ldapsearch with -LLL for LDIF output
if ! ldapsearch -x -H "$SOURCE_HOST" -D "$SOURCE_BIND_DN" -w "$SOURCE_BIND_PASS" \
-b "$base" -s sub "(objectClass=*)" > "$outfile" 2>/dev/null; then
warn " No data or base DN not found: $base"
# Create empty file to signal "checked"
echo "# No data for $base" > "$outfile"
fi
}
# Export standard subtrees
export_subtree "ou=people,$BASE_DN" "$EXPORT_DIR/01-people.ldif"
export_subtree "ou=groups,$BASE_DN" "$EXPORT_DIR/02-groups.ldif"
export_subtree "ou=sudoers,$BASE_DN" "$EXPORT_DIR/03-sudoers.ldif"
export_subtree "ou=services,$BASE_DN" "$EXPORT_DIR/04-services.ldif"
# Also export cn=config for reference (read-only, won't import)
info " Exporting cn=config for reference..."
ldapsearch -x -H "$SOURCE_HOST" -D "$SOURCE_BIND_DN" -w "$SOURCE_BIND_PASS" \
-b "cn=config" -s sub "(objectClass=*)" > "$EXPORT_DIR/00-config-reference.ldif" 2>/dev/null || true
# Count entries
for f in "$EXPORT_DIR"/*.ldif; do
count=$(grep -c "^dn:" "$f" 2>/dev/null || echo 0)
info " $(basename "$f"): $count entries"
done
success "Export complete: $EXPORT_DIR"
# ── Phase 2: Transform LDIF ──────────────────────────────────────────────────
info "Phase 2: Transforming LDIF for theta42 compatibility..."
# Create transformation script
cat > "$EXPORT_DIR/transform.sh" <<'TRANSFORM_SCRIPT'
#!/usr/bin/env bash
# Transform exported LDIF for theta42 compatibility
INPUT="$1"
OUTPUT="$2"
BASE_DN="$3"
# theta42 requires certain objectClasses and attributes
# This script:
# 1. Ensures posixAccount has uidNumber, gidNumber, homeDirectory, loginShell
# 2. Ensures groupOfNames has at least one member
# 3. Adds ldapPublicKey objectClass where sshPublicKey exists
# 4. Normalizes password hash formats if needed
while IFS= read -r line || [[ -n "$line" ]]; do
echo "$line"
done < "$INPUT" > "$OUTPUT"
echo "Transform complete: $OUTPUT"
TRANSFORM_SCRIPT
chmod +x "$EXPORT_DIR/transform.sh"
# For now, we'll do a direct import. The transformation is minimal for most setups.
# If you have custom schemas, you may need to edit the LDIF manually.
# ── Phase 3: Prepare theta42 LDAP ────────────────────────────────────────────
info "Phase 3: Preparing theta42 LDAP..."
# Stop theta42 stack
COMPOSE_CMD=""
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
else
die "docker compose not found"
fi
info " Stopping sso-manager container..."
$COMPOSE_CMD stop sso-manager 2>/dev/null || true
# Wait for container to stop
sleep 3
# ── Phase 4: Import into theta42 ─────────────────────────────────────────────
info "Phase 4: Importing data into theta42 LDAP..."
# Create import script that runs inside the container
cat > "$EXPORT_DIR/import-to-theta42.sh" <<'IMPORT_SCRIPT'
#!/bin/bash
# Run inside theta42 sso-manager container to import LDIF
set -e
EXPORT_DIR="$1"
BASE_DN="$2"
# Stop slapd if running
pkill slapd 2>/dev/null || true
sleep 2
# Clear existing data (but preserve structure)
info "Clearing existing LDAP data..."
rm -rf /var/lib/ldap/*
rm -rf /var/lib/ldap/db.*
# Initialize LDAP database with theta42 schema
info "Initializing LDAP database..."
# Create initial LDIF with base structure
cat > /tmp/base.ldif <<EOF
dn: $BASE_DN
objectClass: top
objectClass: dcObject
objectClass: organization
dc: $(echo $BASE_DN | sed 's/,dc=.*//; s/dc=//')
o: Organization
dn: ou=people,$BASE_DN
objectClass: organizationalUnit
ou: people
dn: ou=groups,$BASE_DN
objectClass: organizationalUnit
ou: groups
dn: ou=sudoers,$BASE_DN
objectClass: organizationalUnit
ou: sudoers
dn: ou=services,$BASE_DN
objectClass: organizationalUnit
ou: services
dn: cn=admin,$BASE_DN
objectClass: organizationalRole
cn: admin
description: LDAP Administrator
dn: cn=ldap-admin,ou=groups,$BASE_DN
objectClass: groupOfNames
cn: ldap-admin
member: cn=admin,$BASE_DN
EOF
# Import base structure
slapadd -c -l /tmp/base.ldif -b "$BASE_DN" 2>/dev/null || true
# Import user data
for f in "$EXPORT_DIR"/*.ldif; do
[[ -f "$f" ]] || continue
[[ "$(basename "$f")" == "00-config-reference.ldif" ]] && continue
info "Importing $f..."
# Use -c to continue on errors (some entries may already exist)
slapadd -c -l "$f" -b "$BASE_DN" 2>/dev/null || warn "Some entries in $f may have failed"
done
# Fix ownership
chown -R ldap:ldap /var/lib/ldap
# Start slapd
info "Starting slapd..."
exec /usr/sbin/slapd -h "ldap:/// ldaps:///" -u ldap -g ldap
IMPORT_SCRIPT
# Copy import script to export dir
cp "$EXPORT_DIR/import-to-theta42.sh" "$EXPORT_DIR/"
# Run the import inside the container
info "Running import inside sso-manager container..."
# First, start a temporary container to do the import
$COMPOSE_CMD up -d sso-manager 2>/dev/null || true
sleep 5
# Copy LDIF files into container
info "Copying LDIF files to container..."
for f in "$EXPORT_DIR"/*.ldif; do
[[ -f "$f" ]] || continue
docker cp "$f" sso-manager:/tmp/migration/ 2>/dev/null || {
docker exec sso-manager mkdir -p /tmp/migration
docker cp "$f" sso-manager:/tmp/migration/
}
done
# Run import
info "Executing import..."
docker exec sso-manager bash -c "
pkill slapd 2>/dev/null || true
sleep 2
# Clear data
rm -rf /var/lib/ldap/*
# Create base structure
slapadd -c -b '$BASE_DN' <<EOF
dn: $BASE_DN
objectClass: top
objectClass: dcObject
objectClass: organization
dc: $(echo $BASE_DN | cut -d',' -f1 | cut -d'=' -f2)
o: $TARGET_DOMAIN
dn: ou=people,$BASE_DN
objectClass: organizationalUnit
ou: people
dn: ou=groups,$BASE_DN
objectClass: organizationalUnit
ou: groups
dn: ou=sudoers,$BASE_DN
objectClass: organizationalUnit
ou: sudoers
dn: ou=services,$BASE_DN
objectClass: organizationalUnit
ou: services
EOF
# Import user data
for f in /tmp/migration/*.ldif; do
[[ \"\$(basename \$f)\" == \"00-config-reference.ldif\" ]] && continue
[[ -f \"\$f\" ]] || continue
echo \"Importing \$f...\"
slapadd -c -l \"\$f\" -b '$BASE_DN' 2>/dev/null || echo \"Warning: Some entries in \$f may have failed\"
done
# Fix ownership
chown -R ldap:ldap /var/lib/ldap
echo \"Import complete!\"
" || warn "Import had some errors - check output above"
# ── Phase 5: Create theta42 admin groups ─────────────────────────────────────
info "Phase 5: Creating theta42 admin groups..."
# Create LDIF for theta42-specific groups
cat > "$EXPORT_DIR/theta42-groups.ldif" <<EOF
# theta42 administrative groups
# These groups control access to various features
# Cross-app super admin - full admin in all apps
dn: cn=app_super_admin,ou=groups,$BASE_DN
objectClass: groupOfNames
objectClass: top
cn: app_super_admin
description: Cross-app super administrators
# SSO Manager admin
dn: cn=app_sso_admin,ou=groups,$BASE_DN
objectClass: groupOfNames
objectClass: top
cn: app_sso_admin
description: SSO Manager administrators
# SSO invite - can invite users
dn: cn=app_sso_invite,ou=groups,$BASE_DN
objectClass: groupOfNames
objectClass: top
cn: app_sso_invite
description: Can send invitations
# OAuth admin - manages OAuth clients
dn: cn=app_sso_oauth_admin,ou=groups,$BASE_DN
objectClass: groupOfNames
objectClass: top
cn: app_sso_oauth_admin
description: OAuth client administrators
# Service account marker
dn: cn=app_sso_service_account,ou=groups,$BASE_DN
objectClass: groupOfNames
objectClass: top
cn: app_sso_service_account
description: Service accounts (hidden from UI)
# Jump host admin - audit access only
dn: cn=app_jump_admin,ou=groups,$BASE_DN
objectClass: groupOfNames
objectClass: top
cn: app_jump_admin
description: Jump host audit administrators
EOF
# Import the theta42 groups
docker exec sso-manager bash -c "
slapadd -c -l /tmp/theta42-groups.ldif -b '$BASE_DN' 2>/dev/null || echo \"Groups may already exist\"
" <<EOF
$(cat "$EXPORT_DIR/theta42-groups.ldif")
EOF
# ── Phase 6: Restart and verify ──────────────────────────────────────────────
info "Phase 6: Restarting theta42 stack..."
$COMPOSE_CMD restart sso-manager
sleep 10
info "Waiting for sso-manager to be healthy..."
for i in $(seq 1 30); do
if docker exec sso-manager wget -q -O- http://localhost:3001/health >/dev/null 2>&1; then
success "sso-manager is healthy!"
break
fi
if (( i == 30 )); then
warn "sso-manager did not become healthy in 30s. Check logs with: docker compose logs sso-manager"
fi
sleep 2
done
# Verify import
info "Verifying import..."
dn_count=$(docker exec sso-manager ldapsearch -x -H "ldap://localhost" -b "$BASE_DN" -s sub "(objectClass=*)" dn 2>/dev/null | grep -c "^dn:" || echo 0)
info "Total entries in LDAP: $dn_count"
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
success "Migration complete!"
echo ""
info "Summary:"
info " - Exported data saved to: $EXPORT_DIR"
info " - Base DN: $BASE_DN"
info " - Total entries: $dn_count"
echo ""
info "Next steps:"
info " 1. Review the exported LDIF files in $EXPORT_DIR"
info " 2. Add users to theta42 admin groups as needed:"
info " docker exec sso-manager ldapmodify -x -H ldap://localhost -D 'cn=admin,$BASE_DN' -w <admin-pass>"
info " 3. Update your LDAP clients to point to theta42"
info " 4. Run ./setup.sh to complete theta42 bootstrap"
echo ""
warn "IMPORTANT: Update all LDAP clients to use the new theta42 LDAP server!"
warn " - SSSD: Update /etc/sssd/sssd.conf ldap_uri"
warn " - sudo: Update /etc/sudo-ldap.conf"
warn " - Apps: Update LDAP connection strings"