SRE Runbook

OCI Identity Domain
Auth Test Suite

Curl-only test suite for validating RSA API key authentication against two OCI identity domains from a shell. No OCI CLI required. Built for SRE smoke-testing of external integrations — Splunk, Wiz.io, Terraform Enterprise. Includes cross-domain user management scripts.

RSA-SHA256 Signature v1 curl + openssl 26 Tests 11 Files

Suite Overview

Two identity domains, two authentication models — both using OCI API Key Signature v1 (RSA-SHA256), but against different base URLs. The DEFAULT domain is a legacy IAM user hitting /20160918/. The DEVEL domain is an IDCS user hitting /admin/v1/ SCIM endpoints. No OAuth tokens involved anywhere.

26
Total Tests
9
Default Domain
9
Devel Domain
8
Isolation Tests
2
Login Scripts
2
Create Scripts
Test Suite
  run_tests.sh  (orchestrator)
    ├── test_default_domain.sh          9 tests  — legacy IAM /20160918/ endpoints
    ├── test_devel_domain.sh            9 tests  — IDCS /admin/v1/ SCIM endpoints
    └── test_cross_domain.sh            8 tests  — isolation & forgery negative tests

Shared Libraries
    ├── lib_oci_sign.sh                 OCI Signature v1 (RSA-SHA256) + wrappers
    ├── lib_test_helpers.sh             Colored output, counters, summary
    └── env.sh                          All credentials, endpoints & domain OCIDs

Login / Smoke-test Scripts
    ├── login_default.sh                Verify DEFAULT domain auth; export env vars
    └── login_devel.sh                  Verify DEVEL domain auth via IDCS /admin/v1/Me

Cross-Domain User Creation
    ├── create_crosstest_user.sh        <username>  DEFAULT user → creates in DEVEL IDCS
    └── create_crosstest_in_default.sh  <username>  DEVEL user  → creates in DEFAULT IDCS
DomainAuth ModelEndpoint
DefaultLegacy IAM useridentity.<region>.oraclecloud.com/20160918/
DevelIDCS user (OCI Identity Domains)idcs-<guid>.identity.oraclecloud.com/admin/v1/
PrerequisitePurpose
curlHTTP requests to OCI REST APIs
opensslRSA-SHA256 signing of each request
base64Encoding the signature
python3JSON parsing in test assertions

Quick Start

Run the test suite

./run_tests.sh — run all 26 tests across both domains
./run_tests.sh default — default domain only
./run_tests.sh devel — devel domain (IDCS) only
./run_tests.sh cross — isolation tests only

Login / smoke-test a single domain

./login_default.sh — verify DEFAULT domain, print identity
./login_devel.sh — verify DEVEL domain via IDCS, print identity
source ./login_default.sh — same, but also export env vars into shell

Cross-domain user creation

./create_crosstest_user.sh <username> — DEFAULT user creates in DEVEL
./create_crosstest_in_default.sh <username> — DEVEL user creates in DEFAULT
Email is derived as <username>@ryzior.com. Acting identity is displayed before action.

env.sh

Central configuration. Holds credentials, IDCS URLs and domain OCIDs for both domains. Note the two distinct auth endpoints — legacy IAM for Default, IDCS SCIM for Devel.

env.sh 42 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  OCI Auth Test Suite — Environment Configuration
#
#  Two identity domains, two authentication models:
#
#  DEFAULT domain  → legacy IAM user, RSA-signed /20160918/ API
#  DEVEL domain    → IDCS user, RSA-signed /admin/v1/ SCIM API
#
#  Both use OCI API Key Signature v1 (RSA-SHA256), but against
#  different base URLs.
# ─────────────────────────────────────────────────────────────────

# ── Region ──
export OCI_REGION="eu-frankfurt-1"

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  DEFAULT DOMAIN  (Legacy IAM)
#  Auth target: DEFAULT_BASE_URL/20160918/
#  User lives in legacy IAM — not visible via IDCS SCIM API.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
export DEFAULT_TENANCY_OCID="ocid1.tenancy.oc1..aaaaaaaafwvqjti6nzubd3yf6obb2amwmr5c43j72n65gbbfhumrq2wf4f2a"
export DEFAULT_USER_OCID="ocid1.user.oc1..aaaaaaaaxxagu32ikzlgsylotiogywsr7wwa3redkrcfihwcxhke7uuqytuq"
export DEFAULT_FINGERPRINT="c8:ac:83:36:50:13:45:17:e9:bb:2a:41:66:93:57:fb"
export DEFAULT_KEY_PATH="${HOME}/.oci/oci_api_key.pem"
export DEFAULT_BASE_URL="https://identity.${OCI_REGION}.oraclecloud.com"
export DEFAULT_DOMAIN_OCID="ocid1.domain.oc1..aaaaaaaa5v6cgj5ywxg7ycuiexkwa5zodstgyobb7afadvsdr7umkp6nbzfa"
export DEFAULT_IDCS_URL="https://idcs-4a915662984e434495247863514dc115.identity.oraclecloud.com"

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  DEVEL IDENTITY DOMAIN  (OCI Identity Domains / IDCS)
#  Auth target: DEVEL_IDCS_URL/admin/v1/  (SCIM API)
#  User lives in IDCS — NOT accessible via legacy /20160918/ API.
#  Same RSA-SHA256 signing mechanism, different host in signing string.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
export DEVEL_TENANCY_OCID="ocid1.tenancy.oc1..aaaaaaaafwvqjti6nzubd3yf6obb2amwmr5c43j72n65gbbfhumrq2wf4f2a"
export DEVEL_USER_OCID="ocid1.user.oc1..aaaaaaaanwjvwebk3xk4b5fnhyxk5skhy6oesspvtftll6uxftmcbmbrxuuq"
export DEVEL_FINGERPRINT="0a:e3:74:04:8a:66:bd:b7:bc:d2:77:64:ee:eb:80:80"
export DEVEL_KEY_PATH="${HOME}/.oci/ryzior_oci_api_key.pem"
export DEVEL_BASE_URL="https://identity.${OCI_REGION}.oraclecloud.com"
export DEVEL_DOMAIN_OCID="ocid1.domain.oc1..aaaaaaaajmqq63n33q5msygn6pqlxopaoxvplec6vscf4xhkr67wjkyxqtda"
export DEVEL_IDCS_URL="https://idcs-a8490e61856a4816858a942ef5289a6c.identity.oraclecloud.com"

🔑 lib_oci_sign.sh

RSA-SHA256 signing engine. Builds the OCI Signature v1 Authorization header for every request. default_curl() targets the legacy IAM endpoint; devel_curl() targets the IDCS endpoint. The signing algorithm is identical — only the host in the signing string differs.

lib_oci_sign.sh 143 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  OCI API Signature v1 — Shared Signing Library
#
#  Provides oci_signed_curl() which handles RSA-SHA256 request
#  signing for any OCI REST API call.
#
#  Usage:
#    source ./lib_oci_sign.sh
#    oci_signed_curl <METHOD> <FULL_URL> <TENANCY_OCID> <USER_OCID> \
#                    <FINGERPRINT> <KEY_PATH> [REQUEST_BODY]
# ─────────────────────────────────────────────────────────────────

oci_signed_curl() {
  local METHOD="$1"
  local FULL_URL="$2"
  local TENANCY_OCID="$3"
  local USER_OCID="$4"
  local FINGERPRINT="$5"
  local KEY_PATH="$6"
  local BODY="${7:-}"

  # ── Validate key file exists ──
  if [[ ! -f "$KEY_PATH" ]]; then
    echo "{\"error\": \"Private key not found: ${KEY_PATH}\"}" >&2
    return 1
  fi

  # ── Parse URL components ──
  local HOST
  HOST=$(echo "$FULL_URL" | sed 's|https://||' | cut -d'/' -f1)
  local REQUEST_TARGET
  REQUEST_TARGET=$(echo "$FULL_URL" | sed "s|https://${HOST}||")

  # ── Date header (must be RFC 2616) ──
  local DATE
  DATE=$(date -u '+%a, %d %b %Y %H:%M:%S GMT')

  # ── Key ID = tenancy/user/fingerprint ──
  local KEY_ID="${TENANCY_OCID}/${USER_OCID}/${FINGERPRINT}"

  # ── Method must be lowercase in signing string ──
  local METHOD_LOWER
  METHOD_LOWER=$(echo "$METHOD" | tr '[:upper:]' '[:lower:]')

  # ── Build signing string & headers list ──
  local SIGNING_STRING="(request-target): ${METHOD_LOWER} ${REQUEST_TARGET}
date: ${DATE}
host: ${HOST}"
  local HEADERS_LIST="(request-target) date host"

  # ── Extra headers for POST/PUT/PATCH with a body ──
  local CONTENT_TYPE=""
  local CONTENT_LENGTH=""
  local CONTENT_SHA=""

  if [[ -n "$BODY" ]]; then
    CONTENT_TYPE="application/json"
    CONTENT_LENGTH="${#BODY}"
    CONTENT_SHA=$(printf '%s' "$BODY" | openssl dgst -sha256 -binary | base64)

    SIGNING_STRING="${SIGNING_STRING}
content-length: ${CONTENT_LENGTH}
content-type: ${CONTENT_TYPE}
x-content-sha256: ${CONTENT_SHA}"

    HEADERS_LIST="${HEADERS_LIST} content-length content-type x-content-sha256"
  fi

  # ── RSA-SHA256 signature ──
  local SIGNATURE
  SIGNATURE=$(printf '%s' "$SIGNING_STRING" \
    | openssl dgst -sha256 -sign "$KEY_PATH" \
    | base64 \
    | tr -d '\n')

  local AUTH_HEADER="Signature version=\"1\",keyId=\"${KEY_ID}\",algorithm=\"rsa-sha256\",headers=\"${HEADERS_LIST}\",signature=\"${SIGNATURE}\""

  # ── Execute curl ──
  if [[ -n "$BODY" ]]; then
    curl -s -w "\n%{http_code}" -X "${METHOD}" "${FULL_URL}" \
      -H "date: ${DATE}" \
      -H "host: ${HOST}" \
      -H "content-type: ${CONTENT_TYPE}" \
      -H "content-length: ${CONTENT_LENGTH}" \
      -H "x-content-sha256: ${CONTENT_SHA}" \
      -H "Authorization: ${AUTH_HEADER}" \
      -d "$BODY"
  else
    curl -s -w "\n%{http_code}" -X "${METHOD}" "${FULL_URL}" \
      -H "date: ${DATE}" \
      -H "host: ${HOST}" \
      -H "Authorization: ${AUTH_HEADER}"
  fi
}

# ─────────────────────────────────────────────────────────────────
#  Convenience wrappers that pick the right credential set
#
#  DEFAULT domain user authenticates against the legacy IAM API:
#    https://identity.<region>.oraclecloud.com/20160918/
#
#  DEVEL domain user authenticates against the IDCS SCIM API:
#    https://idcs-<guid>.identity.oraclecloud.com/admin/v1/
#
#  The signing algorithm is identical for both — only the host in
#  the signing string and Authorization header differs.
# ─────────────────────────────────────────────────────────────────

# Signs with DEFAULT domain credentials (legacy IAM user)
default_curl() {
  local METHOD="$1"; shift
  local URL="$1"; shift
  local BODY="${1:-}"
  oci_signed_curl "$METHOD" "$URL" \
    "$DEFAULT_TENANCY_OCID" "$DEFAULT_USER_OCID" \
    "$DEFAULT_FINGERPRINT" "$DEFAULT_KEY_PATH" \
    "$BODY"
}

# Signs with DEVEL domain credentials (IDCS user)
# Use with DEVEL_IDCS_URL/admin/v1/ endpoints — not /20160918/
devel_curl() {
  local METHOD="$1"; shift
  local URL="$1"; shift
  local BODY="${1:-}"
  oci_signed_curl "$METHOD" "$URL" \
    "$DEVEL_TENANCY_OCID" "$DEVEL_USER_OCID" \
    "$DEVEL_FINGERPRINT" "$DEVEL_KEY_PATH" \
    "$BODY"
}

# ─────────────────────────────────────────────────────────────────
#  Helper: split curl output into body + http_code
#  Usage:  read_response "$(some_curl_call)" BODY CODE
# ─────────────────────────────────────────────────────────────────
read_response() {
  local RAW="$1"
  local -n _BODY=$2
  local -n _CODE=$3
  _CODE=$(echo "$RAW" | tail -1)
  _BODY=$(echo "$RAW" | sed '$d')
}

lib_test_helpers.sh

Colored terminal output, test counters, and the final summary printer. Tracks PASS / FAIL / WARN / SKIP across all sourced suites.

lib_test_helpers.sh 86 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Test Helper Library — colored output, counters, formatting
# ─────────────────────────────────────────────────────────────────

# Colors
_RED='\033[0;31m'
_GREEN='\033[0;32m'
_YELLOW='\033[0;33m'
_CYAN='\033[0;36m'
_BOLD='\033[1m'
_NC='\033[0m'

# Counters (exported so they accumulate across sourced scripts)
export TESTS_PASS=${TESTS_PASS:-0}
export TESTS_FAIL=${TESTS_FAIL:-0}
export TESTS_WARN=${TESTS_WARN:-0}
export TESTS_SKIP=${TESTS_SKIP:-0}
export TESTS_TOTAL=${TESTS_TOTAL:-0}

section() {
  echo ""
  echo -e "${_BOLD}${_CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${_NC}"
  echo -e "${_BOLD}${_CYAN}  $1${_NC}"
  echo -e "${_BOLD}${_CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${_NC}"
}

test_start() {
  TESTS_TOTAL=$((TESTS_TOTAL + 1))
  echo ""
  echo -e "  ${_BOLD}▶ $1${_NC}"
}

pass() {
  TESTS_PASS=$((TESTS_PASS + 1))
  echo -e "    ${_GREEN}[PASS]${_NC} $1"
}

fail() {
  TESTS_FAIL=$((TESTS_FAIL + 1))
  echo -e "    ${_RED}[FAIL]${_NC} $1"
}

warn() {
  TESTS_WARN=$((TESTS_WARN + 1))
  echo -e "    ${_YELLOW}[WARN]${_NC} $1"
}

skip() {
  TESTS_SKIP=$((TESTS_SKIP + 1))
  echo -e "    ${_YELLOW}[SKIP]${_NC} $1"
}

detail() {
  # Print truncated response body for debugging
  local MSG="$1"
  local MAX_LEN=300
  if [[ ${#MSG} -gt $MAX_LEN ]]; then
    MSG="${MSG:0:$MAX_LEN}... (truncated)"
  fi
  echo -e "    ${_CYAN}→ ${MSG}${_NC}"
}

print_summary() {
  echo ""
  echo -e "${_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${_NC}"
  echo -e "${_BOLD}  TEST SUMMARY${_NC}"
  echo -e "${_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${_NC}"
  echo -e "  Total:    ${TESTS_TOTAL}"
  echo -e "  ${_GREEN}Passed:   ${TESTS_PASS}${_NC}"
  echo -e "  ${_RED}Failed:   ${TESTS_FAIL}${_NC}"
  echo -e "  ${_YELLOW}Warnings: ${TESTS_WARN}${_NC}"
  echo -e "  ${_YELLOW}Skipped:  ${TESTS_SKIP}${_NC}"
  echo ""

  if [[ "$TESTS_FAIL" -gt 0 ]]; then
    echo -e "  ${_RED}${_BOLD}RESULT: FAILURES DETECTED${_NC}"
    return 1
  elif [[ "$TESTS_WARN" -gt 0 ]]; then
    echo -e "  ${_YELLOW}${_BOLD}RESULT: PASSED WITH WARNINGS${_NC}"
    return 0
  else
    echo -e "  ${_GREEN}${_BOLD}RESULT: ALL TESTS PASSED${_NC}"
    return 0
  fi
}

run_tests.sh

Main entry point. Validates prerequisites, sources credentials, prints the domain/endpoint summary, runs selected suites, and exits 0 on success or 1 if any FAIL was recorded.

run_tests.sh 108 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  OCI Identity Auth Test Runner
#
#  Tests RSA-signed API key authentication for:
#    1. Default domain (legacy IAM)
#    2. Devel identity domain
#    3. Cross-domain isolation (negative tests)
#
#  Usage:
#    ./run_tests.sh              # run all suites
#    ./run_tests.sh default      # run only default domain tests
#    ./run_tests.sh devel        # run only devel domain tests
#    ./run_tests.sh cross        # run only cross-domain tests
#    ./run_tests.sh default devel  # run specific suites
# ─────────────────────────────────────────────────────────────────
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ── Pre-flight checks ──
echo ""
echo "╔═══════════════════════════════════════════════════╗"
echo "║   OCI Identity Domain — Auth Test Suite           ║"
echo "║   RSA API Key Signing (Signature v1)              ║"
echo "╚═══════════════════════════════════════════════════╝"
echo ""
echo "Timestamp : $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "Hostname  : $(hostname)"
echo ""

# ── Validate prerequisites ──
MISSING=0
for CMD in curl openssl python3 base64; do
  if ! command -v "$CMD" &>/dev/null; then
    echo "ERROR: Required command '${CMD}' not found in PATH."
    MISSING=1
  fi
done
[[ "$MISSING" -eq 1 ]] && exit 1

# ── Source env — abort early if not configured ──
if [[ ! -f "${SCRIPT_DIR}/env.sh" ]]; then
  echo "ERROR: env.sh not found. Copy env.sh.example and fill in your values."
  exit 1
fi
source "${SCRIPT_DIR}/env.sh"

# ── Validate key files exist ──
for LABEL_KEY in "DEFAULT:${DEFAULT_KEY_PATH}" "DEVEL:${DEVEL_KEY_PATH}"; do
  LABEL="${LABEL_KEY%%:*}"
  KPATH="${LABEL_KEY#*:}"
  if [[ ! -f "$KPATH" ]]; then
    echo "ERROR: ${LABEL} private key not found: ${KPATH}"
    MISSING=1
  fi
done
[[ "$MISSING" -eq 1 ]] && exit 1

echo "Region    : ${OCI_REGION}"
echo ""
echo "Default domain (legacy IAM)"
echo "  API     : ${DEFAULT_BASE_URL}/20160918/"
echo "  User    : ${DEFAULT_USER_OCID:0:40}..."
echo "  Key     : ${DEFAULT_FINGERPRINT}"
echo ""
echo "Devel domain (IDCS / OCI Identity Domains)"
echo "  API     : ${DEVEL_IDCS_URL}/admin/v1/"
echo "  User    : ${DEVEL_USER_OCID:0:40}..."
echo "  Key     : ${DEVEL_FINGERPRINT}"
echo ""

# ── Source shared libraries ──
source "${SCRIPT_DIR}/lib_oci_sign.sh"
source "${SCRIPT_DIR}/lib_test_helpers.sh"

# ── Determine which suites to run ──
SUITES=("$@")
if [[ ${#SUITES[@]} -eq 0 ]]; then
  SUITES=("default" "devel" "cross")
fi

# ── Run requested suites ──
for SUITE in "${SUITES[@]}"; do
  case "$SUITE" in
    default)
      source "${SCRIPT_DIR}/test_default_domain.sh"
      ;;
    devel)
      source "${SCRIPT_DIR}/test_devel_domain.sh"
      ;;
    cross)
      source "${SCRIPT_DIR}/test_cross_domain.sh"
      ;;
    *)
      echo "Unknown suite: ${SUITE}  (valid: default, devel, cross)"
      exit 1
      ;;
  esac
done

# ── Summary ──
print_summary
EXIT_CODE=$?

echo ""
echo "Log timestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
exit $EXIT_CODE

test_default_domain.sh

9 tests against the legacy IAM default domain via /20160918/. Covers user retrieval, tenancy metadata, API keys, auth tokens, groups, policies, compartments, and identity providers.

#TestValidates
T1GET own userAPI key auth works, user is ACTIVE
T2List usersRead access to tenancy user list
T3GET tenancyTenancy metadata readable
T4List API keysCan enumerate keys for the user
T5List auth tokensCan enumerate SWIFT/auth tokens
T6List groupsGroup enumeration works
T7List policiesPolicy enumeration works
T8List compartmentsCompartment visibility
T9List identity providersFederation (SAML2) config readable
test_default_domain.sh 154 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Test Suite: DEFAULT DOMAIN (Legacy IAM)
#  All calls use OCI API Key Signature v1 (RSA-SHA256)
# ─────────────────────────────────────────────────────────────────
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/lib_oci_sign.sh"
source "${SCRIPT_DIR}/lib_test_helpers.sh"

section "DEFAULT DOMAIN — Legacy IAM"

BASE="${DEFAULT_BASE_URL}"

# ──────────────────────────────────────────────
# T1: Get the authenticated user's own details
# ──────────────────────────────────────────────
test_start "T1: GET own user details"
RAW=$(default_curl GET "${BASE}/20160918/users/${DEFAULT_USER_OCID}")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  LIFECYCLE=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('lifecycleState','UNKNOWN'))" 2>/dev/null || echo "PARSE_ERROR")
  if [[ "$LIFECYCLE" == "ACTIVE" ]]; then
    pass "User retrieved — lifecycleState=ACTIVE (HTTP ${CODE})"
  else
    warn "User retrieved but lifecycleState=${LIFECYCLE} (HTTP ${CODE})"
  fi
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T2: List users in the tenancy
# ──────────────────────────────────────────────
test_start "T2: List users in tenancy"
RAW=$(default_curl GET "${BASE}/20160918/users?compartmentId=${DEFAULT_TENANCY_OCID}&limit=5")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
  pass "Listed ${COUNT} user(s) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T3: Get tenancy metadata
# ──────────────────────────────────────────────
test_start "T3: GET tenancy details"
RAW=$(default_curl GET "${BASE}/20160918/tenancies/${DEFAULT_TENANCY_OCID}")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  TNAME=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name','???'))" 2>/dev/null || echo "PARSE_ERROR")
  pass "Tenancy name: ${TNAME} (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T4: List API keys for the user
# ──────────────────────────────────────────────
test_start "T4: List API keys for user"
RAW=$(default_curl GET "${BASE}/20160918/users/${DEFAULT_USER_OCID}/apiKeys/")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  KEY_COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
  pass "Found ${KEY_COUNT} API key(s) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T5: List auth tokens for the user
# ──────────────────────────────────────────────
test_start "T5: List auth tokens for user"
RAW=$(default_curl GET "${BASE}/20160918/users/${DEFAULT_USER_OCID}/authTokens/")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  TOKEN_COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
  pass "Found ${TOKEN_COUNT} auth token(s) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T6: List groups in the tenancy
# ──────────────────────────────────────────────
test_start "T6: List groups in tenancy"
RAW=$(default_curl GET "${BASE}/20160918/groups?compartmentId=${DEFAULT_TENANCY_OCID}&limit=5")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  GRP_COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
  pass "Listed ${GRP_COUNT} group(s) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T7: List policies in the tenancy
# ──────────────────────────────────────────────
test_start "T7: List policies in tenancy"
RAW=$(default_curl GET "${BASE}/20160918/policies?compartmentId=${DEFAULT_TENANCY_OCID}&limit=5")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  POL_COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
  pass "Listed ${POL_COUNT} policy(ies) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T8: List compartments
# ──────────────────────────────────────────────
test_start "T8: List compartments"
RAW=$(default_curl GET "${BASE}/20160918/compartments?compartmentId=${DEFAULT_TENANCY_OCID}&limit=5")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  COMP_COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
  pass "Listed ${COMP_COUNT} compartment(s) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T9: List identity providers (federation)
# ──────────────────────────────────────────────
test_start "T9: List identity providers"
RAW=$(default_curl GET "${BASE}/20160918/identityProviders?compartmentId=${DEFAULT_TENANCY_OCID}&protocol=SAML2&limit=10")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  IDP_COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
  pass "Listed ${IDP_COUNT} IdP(s) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

test_devel_domain.sh

9 tests against the devel identity domain via IDCS /admin/v1/ SCIM endpoints. The devel user is IDCS-only — not present in legacy IAM. T7 uses DEFAULT credentials to verify domain metadata from the IAM control plane. T8 validates the legacy IAM security boundary.

#TestValidates
T1GET own user (/admin/v1/Me)IDCS API key auth works, user active
T2List domain usersUser enumeration in devel domain
T3List domain groupsGroup enumeration in devel domain
T4API key capability flagcanUseApiKeys=true on user profile
T5IDCS domain settingsDomain settings API accessible
T6List domain appsApp/OAuth client enumeration
T7GET domain metadata (default creds)Devel domain visible from IAM control plane
T8Legacy IAM boundary checkIDCS user correctly absent from /20160918/ — or WARN if IAM policy grants access
T9IDCS OIDC discovery (unauthenticated)IDCS layer health
test_devel_domain.sh 186 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Test Suite: DEVEL IDENTITY DOMAIN (OCI Identity Domains / IDCS)
#
#  The devel domain user is an IDCS identity — NOT a legacy IAM user.
#  All authenticated calls go to DEVEL_IDCS_URL/admin/v1/ (SCIM API).
#  The signing algorithm is identical to legacy IAM (RSA-SHA256),
#  but the host in the signing string is the IDCS endpoint, not
#  identity.<region>.oraclecloud.com.
# ─────────────────────────────────────────────────────────────────
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/lib_oci_sign.sh"
source "${SCRIPT_DIR}/lib_test_helpers.sh"

section "DEVEL IDENTITY DOMAIN — IDCS SCIM API"

IDCS="${DEVEL_IDCS_URL}"

# ──────────────────────────────────────────────
# T1: Get the authenticated user's own details
#     IDCS /admin/v1/Me — equivalent of GET own user in legacy IAM
# ──────────────────────────────────────────────
test_start "T1: GET own user via IDCS /admin/v1/Me"
RAW=$(devel_curl GET "${IDCS}/admin/v1/Me")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  ACTIVE=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('active', False))" 2>/dev/null || echo "false")
  USERNAME=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('userName','?'))" 2>/dev/null || echo "?")
  DOMAIN=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('domainOcid','?'))" 2>/dev/null || echo "?")
  if [[ "$ACTIVE" == "True" ]]; then
    pass "User retrieved — ${USERNAME} — active=true (HTTP ${CODE})"
    detail "domainOcid: ${DOMAIN}"
  else
    warn "User retrieved but active=${ACTIVE} (HTTP ${CODE})"
  fi
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T2: List users in the devel domain
# ──────────────────────────────────────────────
test_start "T2: List users in devel domain"
RAW=$(devel_curl GET "${IDCS}/admin/v1/Users?count=10")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  TOTAL=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('totalResults', 0))" 2>/dev/null || echo "0")
  pass "totalResults=${TOTAL} (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T3: List groups in the devel domain
# ──────────────────────────────────────────────
test_start "T3: List groups in devel domain"
RAW=$(devel_curl GET "${IDCS}/admin/v1/Groups?count=10")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  TOTAL=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('totalResults', 0))" 2>/dev/null || echo "0")
  pass "totalResults=${TOTAL} (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T4: Verify user has API key capability
#     IDCS tracks per-user capability flags in the user profile
# ──────────────────────────────────────────────
test_start "T4: Verify API key capability on devel user"
RAW=$(devel_curl GET "${IDCS}/admin/v1/Me")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  CAN_USE=$(echo "$BODY" | python3 -c "
import sys, json
d = json.load(sys.stdin)
caps = d.get('urn:ietf:params:scim:schemas:oracle:idcs:extension:capabilities:User', {})
print(caps.get('canUseApiKeys', False))
" 2>/dev/null || echo "false")
  if [[ "$CAN_USE" == "True" ]]; then
    pass "canUseApiKeys=true (HTTP ${CODE})"
  else
    fail "canUseApiKeys=${CAN_USE} — API key auth would fail"
  fi
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T5: GET IDCS domain settings
# ──────────────────────────────────────────────
test_start "T5: GET IDCS domain settings"
RAW=$(devel_curl GET "${IDCS}/admin/v1/Settings")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  TENANT=$(echo "$BODY" | python3 -c "
import sys, json
d = json.load(sys.stdin)
resources = d.get('Resources', [d])
print(resources[0].get('defaultTrustScope', resources[0].get('id', '?')))
" 2>/dev/null || echo "?")
  pass "Domain settings accessible (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T6: List apps registered in the devel domain
# ──────────────────────────────────────────────
test_start "T6: List apps in devel domain"
RAW=$(devel_curl GET "${IDCS}/admin/v1/Apps?count=5")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  TOTAL=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('totalResults', 0))" 2>/dev/null || echo "0")
  pass "totalResults=${TOTAL} (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T7: GET devel domain metadata via legacy IAM
#     Uses DEFAULT credentials — verifies the devel domain is
#     visible from the IAM control plane (domain lifecycle, type)
# ──────────────────────────────────────────────
test_start "T7: GET devel domain metadata via legacy IAM (default creds)"
RAW=$(default_curl GET "${DEFAULT_BASE_URL}/20160918/domains/${DEVEL_DOMAIN_OCID}")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "200" ]]; then
  DOM_NAME=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('displayName','?'))" 2>/dev/null || echo "?")
  DOM_TYPE=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('type','?'))" 2>/dev/null || echo "?")
  DOM_STATE=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('lifecycleState','?'))" 2>/dev/null || echo "?")
  pass "Domain: ${DOM_NAME} (type=${DOM_TYPE}, state=${DOM_STATE}) (HTTP ${CODE})"
else
  fail "HTTP ${CODE} — expected 200"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T8: List compartments via IAM policy
#     Devel user has 'manage all-resources in tenancy' IAM policy.
#     Legacy IAM /20160918/ rejects IDCS-only users regardless of
#     policy — this test validates that security boundary holds.
# ──────────────────────────────────────────────
test_start "T8: IDCS user → legacy IAM compartments (expect 404 — boundary check)"
RAW=$(devel_curl GET "${DEFAULT_BASE_URL}/20160918/compartments?compartmentId=${DEVEL_TENANCY_OCID}&limit=2")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "404" ]]; then
  pass "Legacy IAM correctly rejects IDCS-only user — HTTP ${CODE}"
elif [[ "$CODE" == "200" ]]; then
  warn "Legacy IAM accepted IDCS user — HTTP ${CODE} (architecture may have changed)"
  detail "$BODY"
else
  warn "HTTP ${CODE} — unexpected response"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# T9: IDCS OIDC discovery (unauthenticated)
#     Smoke-test that the IDCS layer itself is healthy
# ──────────────────────────────────────────────
test_start "T9: IDCS OIDC discovery (unauthenticated)"
OIDC_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  "${IDCS}/.well-known/openid-configuration")

if [[ "$OIDC_CODE" == "200" ]]; then
  pass "IDCS OIDC endpoint reachable (HTTP ${OIDC_CODE})"
else
  fail "IDCS OIDC endpoint returned HTTP ${OIDC_CODE}"
fi

test_cross_domain.sh

8 negative tests verifying domain isolation. Covers identity forgery, IDCS domain isolation, legacy IAM isolation, bogus fingerprints on both endpoints, and clock skew enforcement.

#ScenarioExpected
X1Default key + devel user OCID → devel IDCS (forgery)401
X2Devel key + default user OCID → default IAM (forgery)401
X3Devel creds → default IDCS (no cross-domain IDCS role)401
X4Devel creds → legacy IAM (IDCS user not in legacy IAM)404
X5Default creds → GET devel user via legacy IAM404
X6Bogus fingerprint → default IAM401
X7Bogus fingerprint → devel IDCS401
X8Clock skew +10 min → default IAM401
test_cross_domain.sh 186 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Test Suite: CROSS-DOMAIN ISOLATION (negative tests)
#
#  Validates the security boundaries between the two domains.
#  All tests confirm that invalid or cross-domain credential use
#  is properly rejected.
#
#  Architecture recap:
#    DEFAULT user  → authenticates via legacy IAM /20160918/
#    DEVEL user    → authenticates via IDCS /admin/v1/ SCIM API
#    DEVEL user is NOT present in legacy IAM — completely separate.
# ─────────────────────────────────────────────────────────────────
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/lib_oci_sign.sh"
source "${SCRIPT_DIR}/lib_test_helpers.sh"

section "CROSS-DOMAIN ISOLATION (negative tests)"

# ──────────────────────────────────────────────
# X1: Forge identity on devel IDCS
#     Sign with DEFAULT key but put DEVEL user OCID in keyId.
#     Signature will not validate — IDCS has the default key's
#     public key associated to the DEFAULT user, not DEVEL.
# ──────────────────────────────────────────────
test_start "X1: Forged keyId — default key + devel user OCID → devel IDCS (expect 401)"
RAW=$(oci_signed_curl GET \
  "${DEVEL_IDCS_URL}/admin/v1/Me" \
  "$DEVEL_TENANCY_OCID" "$DEVEL_USER_OCID" \
  "$DEFAULT_FINGERPRINT" "$DEFAULT_KEY_PATH")
read_response "$RAW" BODY CODE

if [[ "$CODE" =~ ^(401|403)$ ]]; then
  pass "Forged keyId rejected — HTTP ${CODE}"
else
  fail "SECURITY CONCERN — forged keyId accepted! HTTP ${CODE}"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# X2: Forge identity on legacy IAM
#     Sign with DEVEL key but put DEFAULT user OCID in keyId.
# ──────────────────────────────────────────────
test_start "X2: Forged keyId — devel key + default user OCID → default IAM (expect 401)"
RAW=$(oci_signed_curl GET \
  "${DEFAULT_BASE_URL}/20160918/users/${DEFAULT_USER_OCID}" \
  "$DEFAULT_TENANCY_OCID" "$DEFAULT_USER_OCID" \
  "$DEVEL_FINGERPRINT" "$DEVEL_KEY_PATH")
read_response "$RAW" BODY CODE

if [[ "$CODE" =~ ^(401|403)$ ]]; then
  pass "Forged keyId rejected — HTTP ${CODE}"
else
  fail "SECURITY CONCERN — forged keyId accepted! HTTP ${CODE}"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# X3: IDCS domain isolation
#     Devel user (valid credentials) → default domain IDCS.
#     Requires explicit Identity Domain Administrator role in the
#     default domain to succeed — should be rejected without it.
# ──────────────────────────────────────────────
test_start "X3: Devel creds → default IDCS /admin/v1/Me (expect 401 — no cross-domain IDCS role)"
RAW=$(devel_curl GET "${DEFAULT_IDCS_URL}/admin/v1/Me")
read_response "$RAW" BODY CODE

if [[ "$CODE" =~ ^(401|403)$ ]]; then
  pass "Default IDCS correctly rejected devel user — HTTP ${CODE}"
elif [[ "$CODE" == "200" ]]; then
  warn "Devel user has cross-domain IDCS access — HTTP ${CODE} (verify this is intentional)"
  detail "$BODY"
else
  warn "HTTP ${CODE} — unexpected"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# X4: Legacy IAM isolation
#     DEVEL user is IDCS-only — legacy IAM has no record of them.
#     Any request to /20160918/ with devel credentials must fail.
# ──────────────────────────────────────────────
test_start "X4: Devel creds → legacy IAM GET user (expect 404 — IDCS user not in legacy IAM)"
RAW=$(devel_curl GET "${DEFAULT_BASE_URL}/20160918/users/${DEVEL_USER_OCID}")
read_response "$RAW" BODY CODE

if [[ "$CODE" =~ ^(401|403|404)$ ]]; then
  pass "Legacy IAM correctly rejected IDCS-only user — HTTP ${CODE}"
else
  fail "SECURITY CONCERN — legacy IAM accepted IDCS user! HTTP ${CODE}"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# X5: Default creds → attempt to read devel user via legacy IAM
#     The devel user OCID does not exist in legacy IAM — must 404.
# ──────────────────────────────────────────────
test_start "X5: Default creds → GET devel user via legacy IAM (expect 404 — IDCS user absent)"
RAW=$(default_curl GET "${DEFAULT_BASE_URL}/20160918/users/${DEVEL_USER_OCID}")
read_response "$RAW" BODY CODE

if [[ "$CODE" =~ ^(401|403|404)$ ]]; then
  pass "IDCS user correctly absent from legacy IAM — HTTP ${CODE}"
else
  fail "SECURITY CONCERN — IDCS user found in legacy IAM! HTTP ${CODE}"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# X6: Bogus fingerprint — default domain
# ──────────────────────────────────────────────
test_start "X6: Bogus fingerprint → default IAM (expect 401)"
RAW=$(oci_signed_curl GET \
  "${DEFAULT_BASE_URL}/20160918/users/${DEFAULT_USER_OCID}" \
  "$DEFAULT_TENANCY_OCID" "$DEFAULT_USER_OCID" \
  "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff" \
  "$DEFAULT_KEY_PATH")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "401" ]]; then
  pass "Bogus fingerprint rejected — HTTP ${CODE}"
else
  fail "Bogus fingerprint NOT rejected — HTTP ${CODE}"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# X7: Bogus fingerprint — devel IDCS
# ──────────────────────────────────────────────
test_start "X7: Bogus fingerprint → devel IDCS (expect 401)"
RAW=$(oci_signed_curl GET \
  "${DEVEL_IDCS_URL}/admin/v1/Me" \
  "$DEVEL_TENANCY_OCID" "$DEVEL_USER_OCID" \
  "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff" \
  "$DEVEL_KEY_PATH")
read_response "$RAW" BODY CODE

if [[ "$CODE" == "401" ]]; then
  pass "Bogus fingerprint rejected — HTTP ${CODE}"
else
  fail "Bogus fingerprint NOT rejected — HTTP ${CODE}"
  detail "$BODY"
fi

# ──────────────────────────────────────────────
# X8: Clock skew (+10 min) — default domain
#     OCI enforces ±5 min tolerance on request timestamps.
# ──────────────────────────────────────────────
test_start "X8: Clock skew +10 min → default IAM (expect 401)"

SKEWED_DATE=$(date -u -d '+10 minutes' '+%a, %d %b %Y %H:%M:%S GMT' 2>/dev/null \
  || date -u -v+10M '+%a, %d %b %Y %H:%M:%S GMT' 2>/dev/null \
  || echo "")

if [[ -n "$SKEWED_DATE" ]]; then
  HOST=$(echo "${DEFAULT_BASE_URL}" | sed 's|https://||' | cut -d'/' -f1)
  REQUEST_TARGET="/20160918/users/${DEFAULT_USER_OCID}"
  KEY_ID="${DEFAULT_TENANCY_OCID}/${DEFAULT_USER_OCID}/${DEFAULT_FINGERPRINT}"

  SIGNING_STRING="(request-target): get ${REQUEST_TARGET}
date: ${SKEWED_DATE}
host: ${HOST}"

  SIGNATURE=$(printf '%s' "$SIGNING_STRING" \
    | openssl dgst -sha256 -sign "$DEFAULT_KEY_PATH" \
    | base64 | tr -d '\n')

  AUTH_HEADER="Signature version=\"1\",keyId=\"${KEY_ID}\",algorithm=\"rsa-sha256\",headers=\"(request-target) date host\",signature=\"${SIGNATURE}\""

  SKEW_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET \
    "${DEFAULT_BASE_URL}${REQUEST_TARGET}" \
    -H "date: ${SKEWED_DATE}" \
    -H "host: ${HOST}" \
    -H "Authorization: ${AUTH_HEADER}")

  if [[ "$SKEW_CODE" == "401" ]]; then
    pass "Clock-skewed request rejected — HTTP ${SKEW_CODE}"
  else
    warn "Clock-skewed request returned HTTP ${SKEW_CODE} (OCI allows ±5 min)"
  fi
else
  skip "Could not generate skewed date on this OS"
fi

login_default.sh

Verifies DEFAULT domain authentication by calling GET /20160918/users/{ocid} with the legacy IAM API key. Prints the authenticated user's name, lifecycle state and tenancy. Can be sourced to export env vars into the current shell.

Usage

./login_default.sh — verify and print result
source ./login_default.sh — verify and export env vars into current shell

login_default.sh 51 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Login / auth-verify for the DEFAULT domain (Legacy IAM)
#  Credentials sourced from ~/.oci/config [DEFAULT] profile.
#
#  Usage:
#    ./login_default.sh            — verify and print result
#    source ./login_default.sh     — also export env vars into shell
# ─────────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib_oci_sign.sh"

# ── Credentials from ~/.oci/config [DEFAULT] ──
export OCI_REGION="eu-frankfurt-1"
export DEFAULT_TENANCY_OCID="ocid1.tenancy.oc1..aaaaaaaafwvqjti6nzubd3yf6obb2amwmr5c43j72n65gbbfhumrq2wf4f2a"
export DEFAULT_USER_OCID="ocid1.user.oc1..aaaaaaaaxxagu32ikzlgsylotiogywsr7wwa3redkrcfihwcxhke7uuqytuq"
export DEFAULT_FINGERPRINT="c8:ac:83:36:50:13:45:17:e9:bb:2a:41:66:93:57:fb"
export DEFAULT_KEY_PATH="${HOME}/.oci/oci_api_key.pem"
export DEFAULT_BASE_URL="https://identity.${OCI_REGION}.oraclecloud.com"

# ── Verify key file exists ──
if [[ ! -f "$DEFAULT_KEY_PATH" ]]; then
  echo -e "\033[0;31m[FAIL]\033[0m  Key file not found: ${DEFAULT_KEY_PATH}"
  return 1 2>/dev/null || exit 1
fi

echo "Authenticating to DEFAULT domain (Legacy IAM)..."

RAW=$(oci_signed_curl GET \
  "${DEFAULT_BASE_URL}/20160918/users/${DEFAULT_USER_OCID}" \
  "$DEFAULT_TENANCY_OCID" "$DEFAULT_USER_OCID" \
  "$DEFAULT_FINGERPRINT" "$DEFAULT_KEY_PATH")

HTTP_CODE=$(echo "$RAW" | tail -1)
BODY=$(echo "$RAW" | sed '$d')

if [[ "$HTTP_CODE" == "200" ]]; then
  DISPLAY_NAME=$(echo "$BODY" | python3 -c \
    "import sys,json; d=json.load(sys.stdin); print(d.get('name', d.get('description','?')))" 2>/dev/null || echo "?")
  LIFECYCLE=$(echo "$BODY" | python3 -c \
    "import sys,json; print(json.load(sys.stdin).get('lifecycleState','?'))" 2>/dev/null || echo "?")
  echo -e "\033[0;32m[OK]\033[0m    DEFAULT domain login verified"
  echo "        User:   ${DISPLAY_NAME}"
  echo "        State:  ${LIFECYCLE}"
  echo "        Tenant: ${DEFAULT_TENANCY_OCID}"
else
  echo -e "\033[0;31m[FAIL]\033[0m  HTTP ${HTTP_CODE} — authentication failed"
  echo "$BODY"
  return 1 2>/dev/null || exit 1
fi

login_devel.sh

Verifies DEVEL domain authentication via the IDCS /admin/v1/Me endpoint. The devel user is IDCS-only — not accessible via legacy IAM. Exports DEVEL_IDCS_URL and DEVEL_DOMAIN_OCID when sourced.

Usage

./login_devel.sh — verify and print result
source ./login_devel.sh — verify and export env vars into current shell

login_devel.sh 59 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Login / auth-verify for the DEVEL identity domain
#  Credentials sourced from ~/.oci/config [ryzior_devel] profile.
#
#  The devel domain uses the new IDCS model — users live under the
#  IDCS SCIM endpoint, not the legacy IAM /20160918/users/ path.
#  Authentication still uses OCI API Key Signature v1 (RSA-SHA256).
#
#  Usage:
#    ./login_devel.sh              — verify and print result
#    source ./login_devel.sh       — also export env vars into shell
# ─────────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib_oci_sign.sh"

# ── Credentials from ~/.oci/config [ryzior_devel] ──
export OCI_REGION="eu-frankfurt-1"
export DEVEL_TENANCY_OCID="ocid1.tenancy.oc1..aaaaaaaafwvqjti6nzubd3yf6obb2amwmr5c43j72n65gbbfhumrq2wf4f2a"
export DEVEL_USER_OCID="ocid1.user.oc1..aaaaaaaanwjvwebk3xk4b5fnhyxk5skhy6oesspvtftll6uxftmcbmbrxuuq"
export DEVEL_FINGERPRINT="0a:e3:74:04:8a:66:bd:b7:bc:d2:77:64:ee:eb:80:80"
export DEVEL_KEY_PATH="${HOME}/.oci/ryzior_oci_api_key.pem"
export DEVEL_BASE_URL="https://identity.${OCI_REGION}.oraclecloud.com"
export DEVEL_DOMAIN_OCID="ocid1.domain.oc1..aaaaaaaajmqq63n33q5msygn6pqlxopaoxvplec6vscf4xhkr67wjkyxqtda"
export DEVEL_IDCS_URL="https://idcs-a8490e61856a4816858a942ef5289a6c.identity.oraclecloud.com"

# ── Verify key file exists ──
if [[ ! -f "$DEVEL_KEY_PATH" ]]; then
  echo -e "\033[0;31m[FAIL]\033[0m  Key file not found: ${DEVEL_KEY_PATH}"
  return 1 2>/dev/null || exit 1
fi

echo "Authenticating to DEVEL identity domain (IDCS)..."

RAW=$(oci_signed_curl GET \
  "${DEVEL_IDCS_URL}/admin/v1/Me" \
  "$DEVEL_TENANCY_OCID" "$DEVEL_USER_OCID" \
  "$DEVEL_FINGERPRINT" "$DEVEL_KEY_PATH")

HTTP_CODE=$(echo "$RAW" | tail -1)
BODY=$(echo "$RAW" | sed '$d')

if [[ "$HTTP_CODE" == "200" ]]; then
  DISPLAY_NAME=$(echo "$BODY" | python3 -c \
    "import sys,json; print(json.load(sys.stdin).get('displayName','?'))" 2>/dev/null || echo "?")
  USER_NAME=$(echo "$BODY" | python3 -c \
    "import sys,json; print(json.load(sys.stdin).get('userName','?'))" 2>/dev/null || echo "?")
  ACTIVE=$(echo "$BODY" | python3 -c \
    "import sys,json; print('ACTIVE' if json.load(sys.stdin).get('active') else 'INACTIVE')" 2>/dev/null || echo "?")
  echo -e "\033[0;32m[OK]\033[0m    DEVEL domain login verified"
  echo "        User:   ${DISPLAY_NAME} (${USER_NAME})"
  echo "        State:  ${ACTIVE}"
  echo "        Domain: ${DEVEL_DOMAIN_OCID}"
else
  echo -e "\033[0;31m[FAIL]\033[0m  HTTP ${HTTP_CODE} — authentication failed"
  echo "$BODY"
  return 1 2>/dev/null || exit 1
fi

+ create_crosstest_user.sh

Authenticates as the DEFAULT domain user (legacy IAM) and creates a new user in the DEVEL identity domain via the IDCS /admin/v1/Users SCIM endpoint. The default user's API key is trusted by the devel IDCS because they hold tenancy-level admin. Email is derived as <username>@ryzior.com. Displays the acting identity before creating the user.

Usage

./create_crosstest_user.sh <username>
Example: ./create_crosstest_user.sh alice → creates alice / alice@ryzior.com in DEVEL domain

create_crosstest_user.sh 94 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Create user 'crosstest' in the DEVEL identity domain
#  authenticated as the DEFAULT domain user (admin).
#
#  Signing: OCI API Key Signature v1 (RSA-SHA256), default creds.
#  Target:  DEVEL domain IDCS SCIM endpoint /admin/v1/Users
# ─────────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib_oci_sign.sh"

# ── DEFAULT domain credentials (from ~/.oci/config [DEFAULT]) ──
TENANCY_OCID="ocid1.tenancy.oc1..aaaaaaaafwvqjti6nzubd3yf6obb2amwmr5c43j72n65gbbfhumrq2wf4f2a"
DEFAULT_USER_OCID="ocid1.user.oc1..aaaaaaaaxxagu32ikzlgsylotiogywsr7wwa3redkrcfihwcxhke7uuqytuq"
DEFAULT_FINGERPRINT="c8:ac:83:36:50:13:45:17:e9:bb:2a:41:66:93:57:fb"
DEFAULT_KEY_PATH="${HOME}/.oci/oci_api_key.pem"

DEFAULT_IAM_URL="https://identity.eu-frankfurt-1.oraclecloud.com"
DEVEL_IDCS_URL="https://idcs-a8490e61856a4816858a942ef5289a6c.identity.oraclecloud.com"

USERNAME="${1:-}"
if [[ -z "$USERNAME" ]]; then
  echo "Usage: $0 <username>"
  exit 1
fi
EMAIL="${USERNAME}@ryzior.com"

if [[ ! -f "$DEFAULT_KEY_PATH" ]]; then
  echo -e "\033[0;31m[FAIL]\033[0m  Key file not found: ${DEFAULT_KEY_PATH}"
  exit 1
fi

# ── Step 1: resolve and display the acting identity ──
RAW=$(oci_signed_curl GET "${DEFAULT_IAM_URL}/20160918/users/${DEFAULT_USER_OCID}" \
  "$TENANCY_OCID" "$DEFAULT_USER_OCID" "$DEFAULT_FINGERPRINT" "$DEFAULT_KEY_PATH")
HTTP_CODE=$(echo "$RAW" | tail -1)
ME=$(echo "$RAW" | sed '$d')

if [[ "$HTTP_CODE" != "200" ]]; then
  echo -e "\033[0;31m[FAIL]\033[0m  Could not resolve acting identity (HTTP ${HTTP_CODE})"
  echo "$ME"
  exit 1
fi

ACTING_NAME=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name','?'))" 2>/dev/null)
ACTING_EMAIL=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('email','?'))" 2>/dev/null)
ACTING_OCID=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null)
ACTING_STATE=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('lifecycleState','?'))" 2>/dev/null)

echo "Acting identity (DEFAULT domain):"
echo "  name:   ${ACTING_NAME} (${ACTING_EMAIL})"
echo "  ocid:   ${ACTING_OCID}"
echo "  state:  ${ACTING_STATE}"
echo "  domain: Default (legacy IAM)"
echo ""

# ── Step 2: create user in DEVEL domain ──
echo "Creating user '${USERNAME}' in DEVEL domain..."

BODY=$(python3 -c "import json,sys; u,e=sys.argv[1],sys.argv[2]; print(json.dumps({
  'schemas': ['urn:ietf:params:scim:schemas:core:2.0:User'],
  'userName': u,
  'name': {'givenName': u, 'familyName': u},
  'emails': [{'value': e, 'type': 'work', 'primary': True}]
}))" "$USERNAME" "$EMAIL")

RAW=$(oci_signed_curl POST "${DEVEL_IDCS_URL}/admin/v1/Users" \
  "$TENANCY_OCID" "$DEFAULT_USER_OCID" \
  "$DEFAULT_FINGERPRINT" "$DEFAULT_KEY_PATH" \
  "$BODY")

HTTP_CODE=$(echo "$RAW" | tail -1)
BODY_RESP=$(echo "$RAW" | sed '$d')

if [[ "$HTTP_CODE" == "201" ]]; then
  NEW_ID=$(echo "$BODY_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null)
  NEW_OCID=$(echo "$BODY_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('ocid','?'))" 2>/dev/null)
  NEW_DOMAIN=$(echo "$BODY_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('domainOcid','?'))" 2>/dev/null)
  echo -e "\033[0;32m[OK]\033[0m    User created"
  echo "        userName: ${USERNAME}"
  echo "        email:    ${EMAIL}"
  echo "        id:       ${NEW_ID}"
  echo "        ocid:     ${NEW_OCID}"
  echo "        domain:   ${NEW_DOMAIN}"
  echo ""
  echo "        Created by: ${ACTING_NAME} (${ACTING_OCID})"
elif [[ "$HTTP_CODE" == "409" ]]; then
  echo -e "\033[0;33m[WARN]\033[0m   User '${USERNAME}' already exists in DEVEL domain (HTTP 409)"
else
  echo -e "\033[0;31m[FAIL]\033[0m  HTTP ${HTTP_CODE}"
  echo "$BODY_RESP"
  exit 1
fi

+ create_crosstest_in_default.sh

Authenticates as the DEVEL domain user (IDCS) and creates a new user in the DEFAULT identity domain via the default domain's IDCS /admin/v1/Users endpoint. Requires IAM policy: ALLOW GROUP 'devel'/'Domain Administrators' to manage all-resources IN TENANCY. Resolves and displays the acting identity (via IDCS /admin/v1/Me) before creating the user.

Usage

./create_crosstest_in_default.sh <username>
Example: ./create_crosstest_in_default.sh bob → creates bob / bob@ryzior.com in DEFAULT domain

create_crosstest_in_default.sh 91 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
#  Create user 'crosstest' in the DEFAULT domain
#  authenticated as the DEVEL domain user (tenancy admin via policy).
#
#  Signing: OCI API Key Signature v1 (RSA-SHA256), devel creds.
#  Target:  DEFAULT domain IDCS SCIM endpoint /admin/v1/Users
# ─────────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib_oci_sign.sh"

# ── DEVEL domain credentials (from ~/.oci/config [ryzior_devel]) ──
TENANCY_OCID="ocid1.tenancy.oc1..aaaaaaaafwvqjti6nzubd3yf6obb2amwmr5c43j72n65gbbfhumrq2wf4f2a"
DEVEL_USER_OCID="ocid1.user.oc1..aaaaaaaanwjvwebk3xk4b5fnhyxk5skhy6oesspvtftll6uxftmcbmbrxuuq"
DEVEL_FINGERPRINT="0a:e3:74:04:8a:66:bd:b7:bc:d2:77:64:ee:eb:80:80"
DEVEL_KEY_PATH="${HOME}/.oci/ryzior_oci_api_key.pem"

DEVEL_IDCS_URL="https://idcs-a8490e61856a4816858a942ef5289a6c.identity.oraclecloud.com"
DEFAULT_IDCS_URL="https://idcs-4a915662984e434495247863514dc115.identity.oraclecloud.com"

USERNAME="${1:-}"
if [[ -z "$USERNAME" ]]; then
  echo "Usage: $0 <username>"
  exit 1
fi
EMAIL="${USERNAME}@ryzior.com"

if [[ ! -f "$DEVEL_KEY_PATH" ]]; then
  echo -e "\033[0;31m[FAIL]\033[0m  Key file not found: ${DEVEL_KEY_PATH}"
  exit 1
fi

# ── Step 1: resolve and display the acting identity ──
RAW=$(oci_signed_curl GET "${DEVEL_IDCS_URL}/admin/v1/Me" \
  "$TENANCY_OCID" "$DEVEL_USER_OCID" "$DEVEL_FINGERPRINT" "$DEVEL_KEY_PATH")
HTTP_CODE=$(echo "$RAW" | tail -1)
ME=$(echo "$RAW" | sed '$d')

if [[ "$HTTP_CODE" != "200" ]]; then
  echo -e "\033[0;31m[FAIL]\033[0m  Could not resolve acting identity (HTTP ${HTTP_CODE})"
  echo "$ME"
  exit 1
fi

ACTING_NAME=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('displayName','?'))" 2>/dev/null)
ACTING_USER=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('userName','?'))" 2>/dev/null)
ACTING_OCID=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('ocid','?'))" 2>/dev/null)
ACTING_DOMAIN=$(echo "$ME" | python3 -c "import sys,json; print(json.load(sys.stdin).get('domainOcid','?'))" 2>/dev/null)

echo "Acting identity:"
echo "  name:   ${ACTING_NAME} (${ACTING_USER})"
echo "  ocid:   ${ACTING_OCID}"
echo "  domain: ${ACTING_DOMAIN}"
echo ""

# ── Step 2: create user in DEFAULT domain ──
echo "Creating user '${USERNAME}' in DEFAULT domain..."

BODY=$(python3 -c "import json,sys; u,e=sys.argv[1],sys.argv[2]; print(json.dumps({
  'schemas': ['urn:ietf:params:scim:schemas:core:2.0:User'],
  'userName': u,
  'name': {'givenName': u, 'familyName': u},
  'emails': [{'value': e, 'type': 'work', 'primary': True}]
}))" "$USERNAME" "$EMAIL")

RAW=$(oci_signed_curl POST "${DEFAULT_IDCS_URL}/admin/v1/Users" \
  "$TENANCY_OCID" "$DEVEL_USER_OCID" "$DEVEL_FINGERPRINT" "$DEVEL_KEY_PATH" \
  "$BODY")
HTTP_CODE=$(echo "$RAW" | tail -1)
BODY_RESP=$(echo "$RAW" | sed '$d')

if [[ "$HTTP_CODE" == "201" ]]; then
  NEW_ID=$(echo "$BODY_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null)
  NEW_OCID=$(echo "$BODY_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('ocid','?'))" 2>/dev/null)
  NEW_DOMAIN=$(echo "$BODY_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('domainOcid','?'))" 2>/dev/null)
  echo -e "\033[0;32m[OK]\033[0m    User created"
  echo "        userName: ${USERNAME}"
  echo "        email:    ${EMAIL}"
  echo "        id:       ${NEW_ID}"
  echo "        ocid:     ${NEW_OCID}"
  echo "        domain:   ${NEW_DOMAIN}"
  echo ""
  echo "        Created by: ${ACTING_NAME} (${ACTING_OCID})"
elif [[ "$HTTP_CODE" == "409" ]]; then
  echo -e "\033[0;33m[WARN]\033[0m   User '${USERNAME}' already exists in DEFAULT domain (HTTP 409)"
else
  echo -e "\033[0;31m[FAIL]\033[0m  HTTP ${HTTP_CODE}"
  echo "$BODY_RESP"
  exit 1
fi

Troubleshooting

Common failure patterns and what they mean.

SymptomCause
401 NotAuthenticatedWrong fingerprint, OCID, or key file mismatch
401 SignatureErrorClock skew > 5 min, or signing string assembled incorrectly
404 on devel domain testsUsing /20160918/ instead of IDCS /admin/v1/ endpoint — devel user is IDCS-only
401 accessDenied on default IDCSDevel user has no Identity Domain Administrator role in the default domain
X3 returns 200Review — devel user has been granted IDCS admin in default domain
T8 devel suite shows WARNIAM policy grants legacy IAM read access to IDCS user — verify if intentional
Cross-domain returns 200 unexpectedlySecurity issue — investigate policy leakage
All cross-domain tests pass (4xx)Good — domains are properly isolated

CI/CD Integration

./run_tests.sh && echo "OCI auth OK" || { echo "ALERT: auth smoke test FAILED"; exit 1; }