Files
ITWorx_MCP_Hub-Public/scripts/contextforge_bootstrap.py
T

337 lines
12 KiB
Python

#!/usr/bin/env python3
"""Create the least-privilege ContextForge API token without exposing it."""
from __future__ import annotations
import argparse
import json
import os
import stat
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
from urllib.parse import quote, urlparse
MAX_RESPONSE_BYTES = 1024 * 1024
RUNTIME_PERMISSIONS = (
"gateways.read",
"gateways.create",
"gateways.update",
"gateways.delete",
"tools.read",
"tools.update",
"tools.execute",
"prompts.read",
"servers.read",
"servers.create",
"servers.update",
"servers.use",
)
RUNTIME_READ_CHECKS = (
"/gateways?limit=1&include_pagination=false",
"/tools?limit=1&include_pagination=false",
"/prompts?limit=1&include_pagination=false",
"/servers?limit=1&include_pagination=false",
)
class BootstrapError(RuntimeError):
"""Sanitized bootstrap failure."""
def _validate_base_url(value: str) -> str:
parsed = urlparse(value)
allowed = (
parsed.scheme == "http"
and parsed.hostname == "contextforge"
and parsed.port == 4444
and not parsed.username
and not parsed.password
) or (
parsed.scheme == "https"
and bool(parsed.hostname)
and not parsed.username
and not parsed.password
)
if not allowed or parsed.path not in {"", "/"} or parsed.query or parsed.fragment:
raise BootstrapError("ContextForge bootstrap URL is not approved")
return value.rstrip("/")
def _read_secret(path: Path) -> str:
try:
metadata = path.stat()
payload = path.read_bytes()
except OSError as exc:
raise BootstrapError("ContextForge admin password is unavailable") from exc
content = payload.rstrip(b"\r\n")
if (
not stat.S_ISREG(metadata.st_mode)
or (
os.name != "nt"
and (
stat.S_IMODE(metadata.st_mode) not in {0o400, 0o600}
or metadata.st_uid != os.getuid()
or metadata.st_gid != os.getgid()
)
)
or not content
or len(content) > 8192
or b"\x00" in content
or b"\r" in content
or b"\n" in content
):
raise BootstrapError("ContextForge admin password violates the secret-file policy")
return content.decode("utf-8")
def _request_json(
base_url: str,
method: str,
path: str,
*,
token: str | None = None,
body: dict[str, Any] | None = None,
) -> Any:
headers = {"Accept": "application/json"}
data = None
if token:
headers["Authorization"] = f"Bearer {token}"
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body, separators=(",", ":")).encode()
request = urllib.request.Request( # noqa: S310 - URL is validated before use.
base_url + path, data=data, headers=headers, method=method
)
try:
with urllib.request.urlopen(request, timeout=10) as response: # noqa: S310
payload = response.read(MAX_RESPONSE_BYTES + 1)
if len(payload) > MAX_RESPONSE_BYTES:
raise BootstrapError("ContextForge bootstrap response exceeded its limit")
return json.loads(payload) if payload else None
except urllib.error.HTTPError as exc:
raise BootstrapError(
f"ContextForge {path.split('?', 1)[0]} returned HTTP {exc.code}"
) from None
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise BootstrapError("ContextForge bootstrap request failed") from exc
def _write_token(path: Path, token: str) -> None:
if not token or "\n" in token or "\r" in token or "\x00" in token or len(token) > 8192:
raise BootstrapError("ContextForge returned an invalid API token")
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
if os.name != "nt" and stat.S_IMODE(path.parent.stat().st_mode) != 0o700:
raise BootstrapError("ContextForge token directory must have mode 0700")
descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
stream.write(token)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
except Exception:
try:
os.close(descriptor)
except OSError:
pass
try:
os.unlink(temporary)
except OSError:
pass
raise
def _login(base_url: str, admin_email: str, password: str) -> str:
login = _request_json(
base_url,
"POST",
"/auth/email/login",
body={"email": admin_email, "password": password},
)
session_token = login.get("access_token") if isinstance(login, dict) else None
if not isinstance(session_token, str) or not session_token:
raise BootstrapError("ContextForge login did not return a session token")
return session_token
def _verify_runtime_token(base_url: str, token: str) -> None:
for path in RUNTIME_READ_CHECKS:
_request_json(base_url, "GET", path, token=token)
def _revoke_token(base_url: str, session_token: str, token_id: str) -> None:
if not token_id:
raise BootstrapError("ContextForge token identifier was invalid")
encoded_id = quote(token_id, safe="")
_request_json(base_url, "DELETE", f"/tokens/{encoded_id}", token=session_token)
def _revoke_named_token(base_url: str, session_token: str, token_name: str) -> None:
catalog = _request_json(
base_url,
"GET",
"/tokens?include_inactive=false&limit=100&offset=0",
token=session_token,
)
tokens = catalog.get("tokens") if isinstance(catalog, dict) else None
if not isinstance(tokens, list):
raise BootstrapError("ContextForge token inventory response was invalid")
matches = [
item
for item in tokens
if isinstance(item, dict)
and item.get("name") == token_name
and item.get("is_active") is True
and item.get("is_revoked") is not True
]
if len(matches) != 1 or not isinstance(matches[0].get("id"), str) or not matches[0]["id"]:
raise BootstrapError("ContextForge token rotation requires one active named token")
_revoke_token(base_url, session_token, matches[0]["id"])
def bootstrap(
*,
base_url: str,
admin_email: str,
admin_password_file: Path,
output_token_file: Path,
source_network: str,
token_name: str,
expires_days: int,
rotate_existing: bool = False,
replacement_token_name: str | None = None,
) -> dict[str, Any]:
base_url = _validate_base_url(base_url)
if rotate_existing and (not replacement_token_name or replacement_token_name == token_name):
raise BootstrapError("ContextForge rotation requires a distinct replacement token name")
effective_token_name = replacement_token_name if rotate_existing else token_name
if not effective_token_name:
raise BootstrapError("ContextForge token name is required")
password: str | None = None
session_token: str | None = None
if output_token_file.exists():
existing = _read_secret(output_token_file)
if rotate_existing:
password = _read_secret(admin_password_file)
session_token = _login(base_url, admin_email, password)
_revoke_named_token(base_url, session_token, token_name)
try:
output_token_file.unlink()
except OSError as exc:
raise BootstrapError(
"ContextForge revoked token file could not be removed"
) from exc
else:
_verify_runtime_token(base_url, existing)
return {"status": "pass", "action": "reused", "tokenName": token_name}
if password is None:
password = _read_secret(admin_password_file)
if session_token is None:
session_token = _login(base_url, admin_email, password)
created = _request_json(
base_url,
"POST",
"/tokens",
token=session_token,
body={
"name": effective_token_name,
"description": "ITWorx MCP Hub production gateway reconciliation",
"expires_in_days": expires_days,
"scope": {
"permissions": list(RUNTIME_PERMISSIONS),
"ip_restrictions": [source_network],
"time_restrictions": {},
"usage_limits": {},
},
"tags": ["itworx-mcp-hub", "production"],
"is_active": True,
},
)
api_token = created.get("access_token") if isinstance(created, dict) else None
token_record = created.get("token") if isinstance(created, dict) else None
token_id = token_record.get("id") if isinstance(token_record, dict) else None
if not isinstance(api_token, str) or not api_token:
try:
_revoke_named_token(base_url, session_token, effective_token_name)
except BootstrapError:
raise BootstrapError(
"ContextForge token creation returned no API token and cleanup failed"
) from None
raise BootstrapError("ContextForge token creation returned no API token")
if not isinstance(token_id, str) or not token_id:
try:
_revoke_named_token(base_url, session_token, effective_token_name)
except BootstrapError:
raise BootstrapError(
"ContextForge token creation returned no identifier and cleanup failed"
) from None
raise BootstrapError("ContextForge token creation returned no identifier")
try:
_verify_runtime_token(base_url, api_token)
_write_token(output_token_file, api_token)
except Exception as exc:
try:
_revoke_token(base_url, session_token, token_id)
except BootstrapError:
raise BootstrapError(
"ContextForge replacement token validation failed and cleanup failed"
) from None
if isinstance(exc, BootstrapError):
raise
raise BootstrapError("ContextForge replacement token could not be stored") from exc
return {
"status": "pass",
"action": "created",
"tokenName": effective_token_name,
"permissions": list(RUNTIME_PERMISSIONS),
"sourceNetwork": source_network,
"expiresDays": expires_days,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", default="http://contextforge:4444")
parser.add_argument("--admin-email", required=True)
parser.add_argument("--admin-password-file", type=Path, required=True)
parser.add_argument("--output-token-file", type=Path, required=True)
parser.add_argument("--source-network", default="203.0.113.0/24")
parser.add_argument("--token-name", default="itworx-mcp-hub-runtime-prod")
parser.add_argument("--expires-days", type=int, default=365)
parser.add_argument(
"--rotate-existing",
action="store_true",
help=(
"revoke exactly one invalid named token before creating a distinctly named replacement"
),
)
parser.add_argument(
"--replacement-token-name",
help="unique replacement name required with --rotate-existing",
)
args = parser.parse_args()
if not 1 <= args.expires_days <= 365:
raise BootstrapError("ContextForge token expiry must be between 1 and 365 days")
result = bootstrap(
base_url=args.base_url,
admin_email=args.admin_email,
admin_password_file=args.admin_password_file,
output_token_file=args.output_token_file,
source_network=args.source_network,
token_name=args.token_name,
expires_days=args.expires_days,
rotate_existing=args.rotate_existing,
replacement_token_name=args.replacement_token_name,
)
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())