Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,336 @@
|
||||
#!/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())
|
||||
Reference in new issue
Block a user