From 9cd08c762ea457b4c72fe6c6d88b5a3a4e3a3f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82odziejczyk?= Date: Wed, 6 May 2026 15:49:39 +0200 Subject: [PATCH] ai-mcp api keys example --- examples/ai-mcp-api-keys/.env | 15 ++ examples/ai-mcp-api-keys/.gitignore | 2 + examples/ai-mcp-api-keys/README.md | 139 +++++++++++++++++ .../ai-mcp-api-keys/confs/elasticsearch.yml | 19 +++ examples/ai-mcp-api-keys/confs/kibana.yml | 17 +++ .../ai-mcp-api-keys/confs/readonlyrest.yml | 94 ++++++++++++ examples/ai-mcp-api-keys/scripts/demo.sh | 143 ++++++++++++++++++ .../scripts/fake-ai-mcp-query.sh | 66 ++++++++ examples/ai-mcp-api-keys/scripts/init.sh | 39 +++++ .../ai-mcp-api-keys/scripts/post-start.sh | 11 ++ 10 files changed, 545 insertions(+) create mode 100644 examples/ai-mcp-api-keys/.env create mode 100644 examples/ai-mcp-api-keys/.gitignore create mode 100644 examples/ai-mcp-api-keys/README.md create mode 100644 examples/ai-mcp-api-keys/confs/elasticsearch.yml create mode 100644 examples/ai-mcp-api-keys/confs/kibana.yml create mode 100644 examples/ai-mcp-api-keys/confs/readonlyrest.yml create mode 100755 examples/ai-mcp-api-keys/scripts/demo.sh create mode 100755 examples/ai-mcp-api-keys/scripts/fake-ai-mcp-query.sh create mode 100755 examples/ai-mcp-api-keys/scripts/init.sh create mode 100755 examples/ai-mcp-api-keys/scripts/post-start.sh diff --git a/examples/ai-mcp-api-keys/.env b/examples/ai-mcp-api-keys/.env new file mode 100644 index 0000000..8fc4484 --- /dev/null +++ b/examples/ai-mcp-api-keys/.env @@ -0,0 +1,15 @@ +# Minimum ReadonlyREST license edition required to run this example (FREE, PRO, ENT). +# If the detected license is lower, run.sh will exit with an error. +ROR_MIN_LICENSE_EDITION=FREE + +# ES/KBN ROR_PLUGIN_SOURCE options: +# API - download ReadonlyREST plugin from API (requires ROR_ES_VERSION / ROR_KBN_VERSION) +# LOCAL_FILE - use a local plugin file (requires ROR_ES_FILE / ROR_KBN_FILE) + +ES_VERSION=9.0.0 +ROR_ES_PLUGIN_SOURCE=API +ROR_ES_VERSION=1.69.1 + +KBN_VERSION=9.0.0 +ROR_KBN_PLUGIN_SOURCE=API +ROR_KBN_VERSION=1.69.1 diff --git a/examples/ai-mcp-api-keys/.gitignore b/examples/ai-mcp-api-keys/.gitignore new file mode 100644 index 0000000..62e631a --- /dev/null +++ b/examples/ai-mcp-api-keys/.gitignore @@ -0,0 +1,2 @@ +# Generated API key files — do not commit +.runtime-keys/ diff --git a/examples/ai-mcp-api-keys/README.md b/examples/ai-mcp-api-keys/README.md new file mode 100644 index 0000000..7e8923f --- /dev/null +++ b/examples/ai-mcp-api-keys/README.md @@ -0,0 +1,139 @@ +# AI/MCP API Keys + +This example demonstrates how ReadonlyREST enables AI/MCP-style access to Elasticsearch using Elasticsearch-native API keys. It shows how to issue per-user, read-only, index-restricted, expirable, and revocable API keys for use by AI assistants or MCP (Model Context Protocol) clients, and how those keys are audited through ROR's audit log. + +## What this example demonstrates + +- Issuing Elasticsearch-native API keys for AI/MCP tool access +- Read-only action enforcement via ROR ACL (`indices:data/read/*`) +- Index scope restriction via ROR ACL (AI/MCP tokens can only reach `*-reports`, not `*-logs`) +- API key expiration (configured at creation time) +- API key revocation and verification that revoked keys are rejected +- Audit logging for API-key-authenticated requests via the `readonlyrest_audit-*` index + +## User simulation + +Alice and Bob are simulated using ReadonlyREST local users with `auth_key` credentials defined in the `users` section of `readonlyrest.yml`. LDAP is intentionally not used in this PoC. In a production deployment, Alice and Bob would correspond to real users from an identity provider (LDAP, SAML, OIDC). The local users stand in for per-user access rules that would be defined in the production ACL. + +## Fake AI/MCP service + +`fake-ai-mcp-query.sh` is not a real MCP server or AI assistant. It is a minimal shell script that simulates an AI/MCP client querying Elasticsearch using a user-specific API key via `Authorization: ApiKey `. In a real deployment, the MCP server or AI assistant would obtain and store the API key and make equivalent Elasticsearch requests. The important aspects of this PoC are authentication, authorization, revocation, expiration, and audit behaviour — not AI logic. + +## Architecture + +``` +simulated alice / bob + | + | each user creates their own API key (alice:alice / bob:bob) + v + fake-ai-mcp-query.sh + | + | Authorization: ApiKey + v + ReadonlyREST + | + | 1. validates Elasticsearch-native API key + | 2. applies read-only action ACL (indices:data/read/*) + | 3. applies AI/MCP index scope (alice-reports, bob-reports) + | 4. writes audit event to readonlyrest_audit-* + v + Elasticsearch + ├── alice-logs (local user access only — outside AI/MCP scope) + ├── bob-logs (local user access only — outside AI/MCP scope) + ├── alice-reports (AI/MCP API key access) + └── bob-reports (AI/MCP API key access) +``` + +ROR enforces the read-only constraint and the AI/MCP index scope (`alice-reports`, `bob-reports`). AI/MCP tokens cannot reach the operational log indices (`alice-logs`, `bob-logs`) regardless of which user's key is used. + +## How to run + +### Start + +From the repository root: + +```bash +./run.sh ai-mcp-api-keys +``` + +### Run the demo + +```bash +cd examples/ai-mcp-api-keys +bash scripts/demo.sh +``` + +`demo.sh` performs the full lifecycle: + +1. Creates Alice's API key (expires in 1 day) +2. Creates Bob's API key (expires in 1 day) +3. Runs all query and write scenarios through `fake-ai-mcp-query.sh` +4. Revokes Alice's API key and verifies the revoked key is rejected + +### Manual fake AI/MCP queries + +After `demo.sh` has run once (to create API keys in `.runtime-keys/`): + +```bash +cd examples/ai-mcp-api-keys + +bash scripts/fake-ai-mcp-query.sh alice alice-reports # allowed +bash scripts/fake-ai-mcp-query.sh alice alice-logs # denied — logs outside AI/MCP scope +bash scripts/fake-ai-mcp-query.sh bob bob-reports # allowed +bash scripts/fake-ai-mcp-query.sh bob alice-logs # denied — logs outside AI/MCP scope +``` + +The `ES` environment variable can be set to override the default Elasticsearch address: + +```bash +ES=https://localhost:19200 bash scripts/fake-ai-mcp-query.sh alice alice-reports +``` + +### Stop and clean up + +From the repository root: + +```bash +./clean.sh +``` + +Generated API key files are stored in `examples/ai-mcp-api-keys/.runtime-keys/` and are excluded from version control via `.gitignore`. + +## Expected results + +| Scenario | Expected | Enforced by | +|---|---|---| +| Alice API key queries `alice-reports` | Allowed (HTTP 200) | ROR ACL | +| Alice API key queries `alice-logs` | Denied (HTTP 403) | ROR ACL — logs outside AI/MCP index scope | +| Bob API key queries `bob-reports` | Allowed (HTTP 200) | ROR ACL | +| Bob API key queries `alice-logs` | Denied (HTTP 403) | ROR ACL — logs outside AI/MCP index scope | +| Alice API key writes to `alice-reports` | Denied (HTTP 403) | ROR ACL — read-only action enforcement | +| Bob API key writes to `bob-reports` | Denied (HTTP 403) | ROR ACL — read-only action enforcement | +| Alice revoked API key queries `alice-reports` | Rejected (HTTP 403) | ROR — key no longer valid | + +## Auditability + +ROR writes an audit event for every request to the `readonlyrest_audit-*` index. The index pattern is created automatically by the init script. To inspect events: + +1. Open [https://localhost:15601/s/default/app/discover](https://localhost:15601/s/default/app/discover) and log in as `admin:admin` +2. Select the `readonlyrest_audit-*` index pattern + +Audit fields visible in this example: + +| Field | Value | +|---|---| +| `user` | `ai-mcp` (the username assigned to the ROR ACL block for all API key requests) | +| `acl_block` | `AI/MCP API key access` (allowed) or `FORBIDDEN` (denied) | +| `action` | e.g. `indices:data/read/search` | +| `indices` | target index | +| `type` | `ALLOWED` or `FORBIDDEN` | + +Note: the audit log records the logical username configured in the `token_authentication` block (`ai-mcp`), not the identity of the API key creator. The API key name and id are not directly available in ROR audit fields in this configuration. Use Elasticsearch's own `GET /_security/api_key` endpoint (with admin credentials) to correlate key ids with named keys. + +## Security notes + +- This is a local PoC. Credentials and generated API keys are for demonstration purposes only. +- API key files are stored in `.runtime-keys/` and are excluded from version control. +- In production, API key issuance should be gated by your identity management system and automated through a controlled service, not run manually. +- Rate limiting and query throttling are not implemented in this PoC. These belong at the API gateway, proxy, MCP layer, or load balancer layer — not within ROR or Elasticsearch. +- This is not a production-ready MCP server implementation. diff --git a/examples/ai-mcp-api-keys/confs/elasticsearch.yml b/examples/ai-mcp-api-keys/confs/elasticsearch.yml new file mode 100644 index 0000000..e263ae6 --- /dev/null +++ b/examples/ai-mcp-api-keys/confs/elasticsearch.yml @@ -0,0 +1,19 @@ +network.host: 0.0.0.0 + +path.repo: /tmp/repositories + +cluster.max_shards_per_node: 10000 + +xpack.security.enabled: true +xpack.security.http.ssl.enabled: true +xpack.security.http.ssl.key: elasticsearch.key +xpack.security.http.ssl.certificate: elasticsearch.crt +xpack.security.http.ssl.certificate_authorities: ca.crt +xpack.security.http.ssl.verification_mode: certificate +xpack.security.http.ssl.client_authentication: optional +xpack.security.transport.ssl.enabled: true +xpack.security.transport.ssl.key: elasticsearch.key +xpack.security.transport.ssl.certificate: elasticsearch.crt +xpack.security.transport.ssl.certificate_authorities: ca.crt +xpack.security.transport.ssl.verification_mode: certificate +xpack.security.transport.ssl.client_authentication: optional diff --git a/examples/ai-mcp-api-keys/confs/kibana.yml b/examples/ai-mcp-api-keys/confs/kibana.yml new file mode 100644 index 0000000..e927d54 --- /dev/null +++ b/examples/ai-mcp-api-keys/confs/kibana.yml @@ -0,0 +1,17 @@ +server.name: ${SERVER_NAME} +server.host: 0.0.0.0 +server.publicBaseUrl: "https://localhost:15601" + +elasticsearch.username: kibana +elasticsearch.password: kibana +elasticsearch.ssl.verificationMode: none + +server.ssl.enabled: true +server.ssl.certificate: /usr/share/kibana/config/kibana.crt +server.ssl.key: /usr/share/kibana/config/kibana.key +server.ssl.redirectHttpFromPort: 80 + +xpack.encryptedSavedObjects.encryptionKey: "min-32-byte-long-strong-encryption-key" + +readonlyrest_kbn.logLevel: info +readonlyrest_kbn.cookiePass: '12312313123213123213123abcdefghijklm' diff --git a/examples/ai-mcp-api-keys/confs/readonlyrest.yml b/examples/ai-mcp-api-keys/confs/readonlyrest.yml new file mode 100644 index 0000000..780c1cb --- /dev/null +++ b/examples/ai-mcp-api-keys/confs/readonlyrest.yml @@ -0,0 +1,94 @@ +readonlyrest: + + audit: + enabled: true + outputs: [index] + + access_control_rules: + + # Kibana internal service account — unrestricted access for Kibana's own requests + - name: "KIBANA" + type: allow + auth_key: kibana:kibana + verbosity: error + + # Admin — full unrestricted access + - name: "Admin" + type: allow + auth_key: admin:admin + kibana: + access: admin + + # Alice — can create and invalidate her own AI/MCP API keys using her own credentials + - name: "Alice - manage own API keys" + type: allow + groups: ["alice-group"] + actions: + - "cluster:admin/xpack/security/api_key/create" + - "cluster:admin/xpack/security/api_key/invalidate" + + # Bob — can create and invalidate his own AI/MCP API keys using his own credentials + - name: "Bob - manage own API keys" + type: allow + groups: ["bob-group"] + actions: + - "cluster:admin/xpack/security/api_key/create" + - "cluster:admin/xpack/security/api_key/invalidate" + + # Prevent every other identity — including API-key-authenticated sessions — from + # creating, listing, or invalidating API keys or service accounts. + # Without this block an AI/MCP token or an unmatched user could call + # _security/api_key and escalate privileges or enumerate keys. + # See: https://docs.readonlyrest.com/examples/fleet#why-the-forbid-block-is-necessary + - name: "Forbid API key and service account management for all others" + type: forbid + actions: + - "cluster:admin/xpack/security/api_key/*" + - "cluster:admin/xpack/security/service_account/*" + + # Alice — simulated per-user local identity (stands in for an LDAP/SAML/OIDC user) + # In production, this user would come from a real identity provider + - name: "Alice local access" + type: allow + groups: ["alice-group"] + indices: ["alice-logs"] + kibana: + access: rw + + # Bob — simulated per-user local identity (stands in for an LDAP/SAML/OIDC user) + # In production, this user would come from a real identity provider + - name: "Bob local access" + type: allow + groups: ["bob-group"] + indices: ["bob-logs"] + kibana: + access: rw + + # AI/MCP access path via Elasticsearch-native API keys + # ROR validates the API key and enforces read-only actions and the index scope. + # alice-reports and bob-reports are dedicated AI/MCP indices, separate from the + # operational log indices (alice-logs, bob-logs) which are inaccessible via API keys. + - name: "AI/MCP API key access" + type: allow + token_authentication: + type: "api-key" + username: "ai-mcp" + indices: ["alice-reports", "bob-reports"] + actions: ["indices:data/read/*", "indices:admin/shards/search_shards"] + + users: + + # Local users simulate per-user identities from a production identity provider. + # LDAP is intentionally not used in this PoC. + + - username: alice + auth_key: alice:alice # it could be an LDAP rule here instead + groups: + - id: "alice-group" + name: "Alice" + + - username: bob + auth_key: bob:bob # it could be an LDAP rule here instead + groups: + - id: "bob-group" + name: "Bob" diff --git a/examples/ai-mcp-api-keys/scripts/demo.sh b/examples/ai-mcp-api-keys/scripts/demo.sh new file mode 100755 index 0000000..a253163 --- /dev/null +++ b/examples/ai-mcp-api-keys/scripts/demo.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Full lifecycle demo: create API keys, run AI/MCP queries, test write restriction, +# revoke a key, verify rejection, and show where audit events land. +set -euo pipefail + +ES="${ES:-https://localhost:19200}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLE_DIR="$(dirname "$SCRIPT_DIR")" +KEYS_DIR="${EXAMPLE_DIR}/.runtime-keys" + +mkdir -p "$KEYS_DIR" + +sep() { printf '\n'; printf '═%.0s' {1..60}; printf '\n\n'; } +step() { printf '\n\033[1m → %s\033[0m\n' "$*"; } +ok() { printf ' \033[32m✓ %s\033[0m\n' "$*"; } +fail() { printf ' \033[31m✗ %s\033[0m\n' "$*"; } +info() { printf ' %s\n' "$*"; } + +sep +printf ' AI/MCP API Keys Demo\n' +printf ' ReadonlyREST + Elasticsearch Native API Keys\n' +sep + +# --- Create Alice's API key --- +# Alice creates her own key using her own credentials. +# Access scope and read-only enforcement are governed entirely by the ROR ACL block. +step "Creating Alice's AI/MCP API key [expires in 1 day]" +ALICE_RESPONSE=$(curl -k -s -u alice:alice \ + -X POST "${ES}/_security/api_key" \ + -H 'Content-Type: application/json' \ + -d '{"name":"alice-ai-mcp","expiration":"1d"}') +ALICE_KEY_ID=$(printf '%s' "$ALICE_RESPONSE" | jq -r '.id') +printf '%s' "$ALICE_RESPONSE" | jq -r '.encoded' > "${KEYS_DIR}/alice.key" +info "Key id: ${ALICE_KEY_ID}" +info "Key name: $(printf '%s' "$ALICE_RESPONSE" | jq -r '.name')" +ok "Alice's API key created and saved to .runtime-keys/alice.key" + +# --- Create Bob's API key --- +# Bob creates his own key using his own credentials. +# Access scope and read-only enforcement are governed entirely by the ROR ACL block. +step "Creating Bob's AI/MCP API key [expires in 1 day]" +BOB_RESPONSE=$(curl -k -s -u bob:bob \ + -X POST "${ES}/_security/api_key" \ + -H 'Content-Type: application/json' \ + -d '{"name":"bob-ai-mcp","expiration":"1d"}') +BOB_KEY_ID=$(printf '%s' "$BOB_RESPONSE" | jq -r '.id') +printf '%s' "$BOB_RESPONSE" | jq -r '.encoded' > "${KEYS_DIR}/bob.key" +info "Key id: ${BOB_KEY_ID}" +info "Key name: $(printf '%s' "$BOB_RESPONSE" | jq -r '.name')" +ok "Bob's API key created and saved to .runtime-keys/bob.key" + +sep +printf ' Query scenarios via fake AI/MCP service\n' +sep + +FAKE_AI="${SCRIPT_DIR}/fake-ai-mcp-query.sh" + +step "Scenario 1: Alice AI/MCP queries alice-reports [expect: allowed]" +bash "$FAKE_AI" alice alice-reports + +step "Scenario 2: Alice AI/MCP queries alice-logs [expect: denied — logs outside AI/MCP scope]" +bash "$FAKE_AI" alice alice-logs + +step "Scenario 3: Bob AI/MCP queries bob-reports [expect: allowed]" +bash "$FAKE_AI" bob bob-reports + +step "Scenario 4: Bob AI/MCP queries alice-logs [expect: denied — logs outside AI/MCP scope]" +bash "$FAKE_AI" bob alice-logs + +sep +printf ' Write restriction scenarios\n' +sep + +step "Scenario 5: Alice AI/MCP writes to alice-reports [expect: denied — read-only]" +ALICE_KEY=$(cat "${KEYS_DIR}/alice.key") +HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: ApiKey ${ALICE_KEY}" \ + -H 'Content-Type: application/json' \ + -X POST "${ES}/alice-reports/_doc" \ + -d '{"message":"unauthorized write attempt"}') +if [ "$HTTP_CODE" = "403" ]; then + ok "Write blocked (HTTP 403) — ROR read-only action enforcement works" +else + fail "Unexpected HTTP ${HTTP_CODE} (expected 403)" +fi + +step "Scenario 6: Bob AI/MCP writes to bob-reports [expect: denied — read-only]" +BOB_KEY=$(cat "${KEYS_DIR}/bob.key") +HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: ApiKey ${BOB_KEY}" \ + -H 'Content-Type: application/json' \ + -X POST "${ES}/bob-reports/_doc" \ + -d '{"message":"unauthorized write attempt"}') +if [ "$HTTP_CODE" = "403" ]; then + ok "Write blocked (HTTP 403) — ROR read-only action enforcement works" +else + fail "Unexpected HTTP ${HTTP_CODE} (expected 403)" +fi + +sep +printf ' API key revocation\n' +sep + +step "Revoking Alice's API key using her own credentials [id: ${ALICE_KEY_ID}]" +REVOKE_RESPONSE=$(curl -k -s -u alice:alice \ + -X DELETE "${ES}/_security/api_key" \ + -H 'Content-Type: application/json' \ + -d "{\"ids\":[\"${ALICE_KEY_ID}\"]}") +INVALIDATED=$(printf '%s' "$REVOKE_RESPONSE" | jq -r '.invalidated // 0') +info "Keys invalidated: ${INVALIDATED}" +ok "Alice's API key revoked" + +step "Verifying revoked key is rejected [expect: 403]" +HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: ApiKey ${ALICE_KEY}" \ + "${ES}/alice-reports/_search") +if [ "$HTTP_CODE" = "403" ]; then + ok "Revoked key rejected (HTTP 403) — revocation confirmed" +else + fail "Unexpected HTTP ${HTTP_CODE} (expected 403)" +fi + +sep +printf ' Audit log\n' +sep + +info "ROR writes an audit event for every request to the readonlyrest_audit-* index." +info "The index pattern was created automatically. Open Kibana Discover to explore:" +info "" +info " URL: https://localhost:15601/s/default/app/discover" +info " Login: admin:admin" +info " Pattern: readonlyrest_audit-*" +info "" +info "Audit fields visible in this example:" +info " user — username assigned to the request (ai-mcp for API key requests)" +info " acl_block — which ROR ACL block matched" +info " action — Elasticsearch action (e.g. indices:data/read/search)" +info " indices — target indices" +info " type — ALLOWED or FORBIDDEN" + +sep +printf ' Demo complete.\n' +sep diff --git a/examples/ai-mcp-api-keys/scripts/fake-ai-mcp-query.sh b/examples/ai-mcp-api-keys/scripts/fake-ai-mcp-query.sh new file mode 100755 index 0000000..56b44bc --- /dev/null +++ b/examples/ai-mcp-api-keys/scripts/fake-ai-mcp-query.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Simulates an AI/MCP service querying Elasticsearch using a user-specific API key. +# This is not a real MCP server or AI assistant — it demonstrates the security pattern. +# +# Usage: bash scripts/fake-ai-mcp-query.sh +# e.g.: bash scripts/fake-ai-mcp-query.sh alice alice-reports # allowed +# bash scripts/fake-ai-mcp-query.sh alice alice-logs # denied — logs outside AI/MCP scope +# bash scripts/fake-ai-mcp-query.sh bob bob-reports # allowed +# bash scripts/fake-ai-mcp-query.sh bob alice-logs # denied — logs outside AI/MCP scope +set -euo pipefail + +USER="${1:-}" +INDEX="${2:-}" + +if [ -z "$USER" ] || [ -z "$INDEX" ]; then + printf 'Usage: %s \n' "$0" + printf ' e.g.: %s alice alice-reports\n' "$0" + exit 1 +fi + +ES="${ES:-https://localhost:19200}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLE_DIR="$(dirname "$SCRIPT_DIR")" +KEY_FILE="${EXAMPLE_DIR}/.runtime-keys/${USER}.key" + +if [ ! -f "$KEY_FILE" ]; then + printf ' [fake-ai-mcp] ERROR: No API key found for user "%s"\n' "$USER" + printf ' [fake-ai-mcp] Run scripts/demo.sh first to generate API keys.\n' + exit 1 +fi + +API_KEY=$(cat "$KEY_FILE") + +printf ' [fake-ai-mcp] user=%-8s index=%s\n' "$USER" "$INDEX" +printf ' [fake-ai-mcp] Authorization: ApiKey \n' +printf ' [fake-ai-mcp] GET %s/%s/_search\n' "$ES" "$INDEX" + +RESPONSE=$(curl -k -s -w '\n%{http_code}' \ + -H "Authorization: ApiKey ${API_KEY}" \ + "${ES}/${INDEX}/_search") + +HTTP_CODE=$(printf '%s' "$RESPONSE" | tail -n1) +BODY=$(printf '%s' "$RESPONSE" | sed '$d') + +case "$HTTP_CODE" in + 200) + printf ' [fake-ai-mcp] \033[32mSUCCESS\033[0m (HTTP %s)\n' "$HTTP_CODE" + printf '%s\n' "$BODY" \ + | jq -r '.hits.hits[] | " hit → \(._source | tojson)"' 2>/dev/null \ + || printf '%s\n' "$BODY" + ;; + 403) + printf ' [fake-ai-mcp] \033[31mDENIED\033[0m (HTTP %s) — access forbidden\n' "$HTTP_CODE" + REASON=$(printf '%s\n' "$BODY" \ + | jq -r '.error.reason // .error.root_cause[0].reason // "no reason in response"' 2>/dev/null \ + || printf 'could not parse response') + printf ' reason: %s\n' "$REASON" + ;; + 401) + printf ' [fake-ai-mcp] \033[31mREJECTED\033[0m (HTTP %s) — invalid or revoked API key\n' "$HTTP_CODE" + ;; + *) + printf ' [fake-ai-mcp] UNEXPECTED (HTTP %s)\n' "$HTTP_CODE" + printf '%s\n' "$BODY" + ;; +esac diff --git a/examples/ai-mcp-api-keys/scripts/init.sh b/examples/ai-mcp-api-keys/scripts/init.sh new file mode 100755 index 0000000..9113185 --- /dev/null +++ b/examples/ai-mcp-api-keys/scripts/init.sh @@ -0,0 +1,39 @@ +#!/bin/bash -ex + +set -o pipefail + +source /usr/local/lib/ror-utils.sh + +# Operational log indices — accessed by local users (alice:alice / bob:bob) +createIndex "alice-logs" +echo '{"user":"alice","message":"Alice app event - login","level":"INFO","service":"auth","@timestamp":"2024-01-15T10:23:45Z"}' \ + | putDocument "alice-logs" + +createIndex "bob-logs" +echo '{"user":"bob","message":"Bob app event - login","level":"INFO","service":"auth","@timestamp":"2024-01-15T10:24:12Z"}' \ + | putDocument "bob-logs" + +# AI/MCP report indices — accessed exclusively via API keys by the AI/MCP service +createIndex "alice-reports" +echo '{"user":"alice","title":"Q1 summary","content":"Alice Q1 performance report","category":"reports","@timestamp":"2024-01-15T08:00:00Z"}' \ + | putDocument "alice-reports" + +createIndex "bob-reports" +echo '{"user":"bob","title":"Q1 summary","content":"Bob Q1 performance report","category":"reports","@timestamp":"2024-01-15T08:05:00Z"}' \ + | putDocument "bob-reports" + +# Create the readonlyrest_audit-* data view in Kibana so audit events are +# immediately browsable in Discover without any manual setup. +# Kibana starts in parallel with the initializer, so we wait for it first. +echo "Waiting for Kibana..." +until curl -fksS --connect-timeout 3 --max-time 5 \ + -u admin:admin https://kbn-ror:5601/api/features >/dev/null 2>&1; do + sleep 5 +done + +curl -ksS -u admin:admin \ + -X POST "https://kbn-ror:5601/api/data_views/data_view" \ + -H "Content-Type: application/json" \ + -H "kbn-xsrf: true" \ + -d '{"data_view":{"title":"readonlyrest_audit-*","timeFieldName":"@timestamp"}}' \ + || true diff --git a/examples/ai-mcp-api-keys/scripts/post-start.sh b/examples/ai-mcp-api-keys/scripts/post-start.sh new file mode 100755 index 0000000..140351b --- /dev/null +++ b/examples/ai-mcp-api-keys/scripts/post-start.sh @@ -0,0 +1,11 @@ +echo -e "ReadonlyREST AI/MCP API keys example is running." +echo -e "" +echo -e "Elasticsearch: https://localhost:19200" +echo -e "Kibana: https://localhost:15601" +echo -e "" +echo -e "Local users (simulated identity provider users):" +echo -e " alice:alice → read access to alice-logs" +echo -e " bob:bob → read access to bob-logs" +echo -e " admin:admin → full admin access" +echo -e "" +bash "${EXAMPLE_DIR}/scripts/demo.sh" || true