494 lines
23 KiB
Python
494 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""HoloLake enterprise identity, relationship and responsibility receipt service.
|
|
|
|
The service binds only to loopback. Nginx exposes exact routes. Credentials are
|
|
verified against the local Forgejo API and are never stored or logged.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import http.cookiejar
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import re
|
|
import sqlite3
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
import uuid
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
|
|
BIND = os.environ.get("GH_ENTERPRISE_IDENTITY_BIND", "127.0.0.1")
|
|
PORT = int(os.environ.get("GH_ENTERPRISE_IDENTITY_PORT", "8032"))
|
|
DB_PATH = os.environ.get(
|
|
"GH_ENTERPRISE_IDENTITY_DB",
|
|
"/var/lib/guanghu-enterprise-identity/identity.sqlite3",
|
|
)
|
|
REGISTRY_PATH = os.environ.get(
|
|
"GH_ENTERPRISE_IDENTITY_REGISTRY",
|
|
"/etc/guanghu/enterprise-identity-registry.json",
|
|
)
|
|
FORGEJO_USER_API = os.environ.get(
|
|
"GH_ENTERPRISE_FORGEJO_USER_API", "http://127.0.0.1:3341/api/v1/user"
|
|
)
|
|
FORGEJO_WEB_BASE = os.environ.get(
|
|
"GH_ENTERPRISE_FORGEJO_WEB_BASE", "https://guanghu.chat/code"
|
|
).rstrip("/")
|
|
FORGEJO_API_BASE = os.environ.get(
|
|
"GH_ENTERPRISE_FORGEJO_API_BASE", "http://127.0.0.1:3341/api/v1"
|
|
).rstrip("/")
|
|
RECEIPT_KEY = os.environ.get("GH_ENTERPRISE_RECEIPT_KEY", "")
|
|
MAX_BODY = 16_384
|
|
USERNAME = re.compile(r"^[A-Za-z0-9_-]{1,40}$")
|
|
DECISIONS = {"ACCEPT", "REJECT", "DEFER", "ACCEPT_WITH_CHANGES"}
|
|
|
|
|
|
def now() -> int:
|
|
return int(time.time())
|
|
|
|
|
|
def canonical(value: object) -> bytes:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
|
|
|
|
def load_registry() -> dict:
|
|
registry = json.loads(Path(REGISTRY_PATH).read_text(encoding="utf-8"))
|
|
if registry.get("schema") != "guanghu.enterprise-identity-registry/v1":
|
|
raise ValueError("enterprise identity registry schema invalid")
|
|
humans = registry.get("humans")
|
|
if not isinstance(humans, list) or not humans:
|
|
raise ValueError("enterprise identity registry is empty")
|
|
numbers = [item.get("human_number") for item in humans]
|
|
usernames = [item.get("username") for item in humans]
|
|
if len(numbers) != len(set(numbers)) or len(usernames) != len(set(usernames)):
|
|
raise ValueError("enterprise identity registry identities must be unique")
|
|
for item in humans:
|
|
if not USERNAME.fullmatch(str(item.get("username", ""))):
|
|
raise ValueError("enterprise username invalid")
|
|
for persona in item.get("personas", []):
|
|
if persona.get("species") != "AGE" or str(persona.get("current_persona_identity", "")).startswith("AGE-"):
|
|
raise ValueError("AGE is a species and cannot be used as a persona identity number")
|
|
return registry
|
|
|
|
|
|
def database() -> sqlite3.Connection:
|
|
path = Path(DB_PATH)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
db = sqlite3.connect(path)
|
|
db.row_factory = sqlite3.Row
|
|
db.executescript(
|
|
"""
|
|
PRAGMA journal_mode=WAL;
|
|
PRAGMA foreign_keys=ON;
|
|
CREATE TABLE IF NOT EXISTS relationship_receipts (
|
|
receipt_id TEXT PRIMARY KEY,
|
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
human_number TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
registry_version TEXT NOT NULL,
|
|
decision TEXT NOT NULL,
|
|
observed_at INTEGER NOT NULL,
|
|
receipt_hash TEXT NOT NULL,
|
|
receipt_signature TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS responsibility_receipts (
|
|
receipt_id TEXT PRIMARY KEY,
|
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
human_number TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
responsibility_domain TEXT NOT NULL,
|
|
responsibility_version TEXT NOT NULL,
|
|
decision TEXT NOT NULL,
|
|
note TEXT NOT NULL,
|
|
observed_at INTEGER NOT NULL,
|
|
receipt_hash TEXT NOT NULL,
|
|
receipt_signature TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS credential_rotation_receipts (
|
|
receipt_id TEXT PRIMARY KEY,
|
|
human_number TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
observed_at INTEGER NOT NULL,
|
|
receipt_hash TEXT NOT NULL,
|
|
receipt_signature TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS audit (
|
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
observed_at INTEGER NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
human_number_hash TEXT NOT NULL,
|
|
receipt_id TEXT NOT NULL
|
|
);
|
|
"""
|
|
)
|
|
return db
|
|
|
|
|
|
def find_human(registry: dict, number: str) -> dict | None:
|
|
encoded = number.encode("utf-8")
|
|
return next(
|
|
(
|
|
item
|
|
for item in registry["humans"]
|
|
if hmac.compare_digest(item["human_number"].encode("utf-8"), encoded)
|
|
),
|
|
None,
|
|
)
|
|
|
|
|
|
def parse_basic(header: str) -> tuple[str, str] | None:
|
|
if not header.startswith("Basic "):
|
|
return None
|
|
try:
|
|
decoded = base64.b64decode(header[6:], validate=True).decode("utf-8")
|
|
username, password = decoded.split(":", 1)
|
|
except (ValueError, UnicodeDecodeError):
|
|
return None
|
|
if not USERNAME.fullmatch(username) or not password or len(password) > 512:
|
|
return None
|
|
return username, password
|
|
|
|
|
|
def verify_forgejo(username: str, password: str) -> bool:
|
|
request = urllib.request.Request(FORGEJO_USER_API)
|
|
credential = base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
request.add_header("Authorization", f"Basic {credential}")
|
|
request.add_header("Accept", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=10) as response:
|
|
body = json.load(response)
|
|
return response.status == 200 and hmac.compare_digest(str(body.get("login", "")), username)
|
|
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, ValueError):
|
|
return False
|
|
|
|
|
|
def rotate_forgejo_password(username: str, current_password: str, new_password: str) -> bool:
|
|
"""Use Forgejo's own first-login session to rotate a forced-change password.
|
|
|
|
This needs no standing admin token: the old credential opens a normal user
|
|
session and Forgejo itself admits only the forced password-change form.
|
|
"""
|
|
jar = http.cookiejar.CookieJar()
|
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
|
try:
|
|
opener.open(FORGEJO_WEB_BASE + "/user/login", timeout=15).read()
|
|
login = urllib.parse.urlencode(
|
|
{"user_name": username, "password": current_password}
|
|
).encode()
|
|
login_request = urllib.request.Request(
|
|
FORGEJO_WEB_BASE + "/user/login", data=login
|
|
)
|
|
login_request.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
with opener.open(login_request, timeout=15) as response:
|
|
response.read()
|
|
if not urllib.parse.urlparse(response.geturl()).path.endswith(
|
|
"/user/settings/change_password"
|
|
):
|
|
return False
|
|
change = urllib.parse.urlencode(
|
|
{"password": new_password, "retype": new_password}
|
|
).encode()
|
|
change_request = urllib.request.Request(
|
|
FORGEJO_WEB_BASE + "/user/settings/change_password", data=change
|
|
)
|
|
change_request.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
with opener.open(change_request, timeout=15) as response:
|
|
response.read()
|
|
return verify_forgejo(username, new_password)
|
|
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, ValueError):
|
|
return False
|
|
|
|
|
|
def signed_receipt(payload: dict) -> dict:
|
|
if len(RECEIPT_KEY) < 32:
|
|
raise RuntimeError("receipt signing key unavailable")
|
|
body = canonical(payload)
|
|
receipt_hash = hashlib.sha256(body).hexdigest()
|
|
signature = hmac.new(RECEIPT_KEY.encode(), body, hashlib.sha256).hexdigest()
|
|
return {**payload, "receipt_hash": receipt_hash, "receipt_signature": signature}
|
|
|
|
|
|
def stable_receipt_id(prefix: str, human_number: str, idempotency_key: str) -> str:
|
|
"""Keep one receipt path across safe client retries without exposing the key."""
|
|
material = f"{prefix}\n{human_number}\n{idempotency_key}".encode()
|
|
digest = hmac.new(RECEIPT_KEY.encode(), material, hashlib.sha256).hexdigest()[:32]
|
|
return f"{prefix}-{digest.upper()}"
|
|
|
|
|
|
def repository_receipt_path(kind: str, receipt_id: str) -> str:
|
|
if kind not in {"relationship", "responsibility"} or not re.fullmatch(
|
|
r"GH-(?:REL|RESP)-[A-F0-9]{32}", receipt_id
|
|
):
|
|
raise ValueError("repository receipt path input invalid")
|
|
return f".guanghu/receipts/{kind}/{receipt_id}.json"
|
|
|
|
|
|
def project_receipt_to_repository(
|
|
human: dict, username: str, password: str, kind: str, receipt: dict
|
|
) -> dict:
|
|
"""Commit a signed receipt with the human's own Forgejo authority.
|
|
|
|
No administrator token or server-side repository credential is held. A
|
|
retry that finds the deterministic path already present must read back the
|
|
exact bytes before treating the projection as idempotent.
|
|
"""
|
|
repository = str(human["repository"])
|
|
if repository.split("/", 1)[0] != username:
|
|
raise RuntimeError("repository owner does not match authenticated user")
|
|
path = repository_receipt_path(kind, str(receipt["receipt_id"]))
|
|
endpoint = (
|
|
f"{FORGEJO_API_BASE}/repos/{urllib.parse.quote(repository, safe='/')}"
|
|
f"/contents/{urllib.parse.quote(path, safe='/')}"
|
|
)
|
|
content = canonical(receipt) + b"\n"
|
|
authorization = "Basic " + base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
create_body = canonical(
|
|
{
|
|
"branch": "main",
|
|
"content": base64.b64encode(content).decode(),
|
|
"message": f"receipt({kind}): {receipt['receipt_id']}",
|
|
}
|
|
)
|
|
request = urllib.request.Request(endpoint, data=create_body, method="POST")
|
|
request.add_header("Authorization", authorization)
|
|
request.add_header("Accept", "application/json")
|
|
request.add_header("Content-Type", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
result = json.load(response)
|
|
if response.status != 201:
|
|
raise RuntimeError(f"repository projection returned {response.status}")
|
|
commit = str(result.get("commit", {}).get("sha", ""))
|
|
if not re.fullmatch(r"[0-9a-f]{40,64}", commit):
|
|
raise RuntimeError("repository projection commit missing")
|
|
return {
|
|
"state": "COMMITTED",
|
|
"repository": repository,
|
|
"path": path,
|
|
"commit": commit,
|
|
}
|
|
except urllib.error.HTTPError as error:
|
|
if error.code != 422:
|
|
raise RuntimeError(f"repository projection failed: {error.code}") from error
|
|
read_request = urllib.request.Request(endpoint + "?ref=main")
|
|
read_request.add_header("Authorization", authorization)
|
|
read_request.add_header("Accept", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(read_request, timeout=15) as response:
|
|
existing = json.load(response)
|
|
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, ValueError) as error:
|
|
raise RuntimeError("repository projection readback failed") from error
|
|
try:
|
|
existing_content = base64.b64decode(str(existing["content"]), validate=True)
|
|
except (KeyError, ValueError) as error:
|
|
raise RuntimeError("repository projection readback invalid") from error
|
|
if not hmac.compare_digest(existing_content, content):
|
|
raise RuntimeError("repository receipt path already contains different bytes")
|
|
return {
|
|
"state": "IDEMPOTENT_READBACK",
|
|
"repository": repository,
|
|
"path": path,
|
|
"commit": str(existing.get("sha", "")),
|
|
}
|
|
|
|
|
|
def public_projection(registry: dict, human: dict, db: sqlite3.Connection | None = None) -> dict:
|
|
projection = {
|
|
"status": "RESOLVED",
|
|
"canonical_id": human["human_number"],
|
|
"subject": {
|
|
"id": human["human_number"],
|
|
"name": human["display_name"],
|
|
"domain": human["responsibility_domain"],
|
|
},
|
|
"work_entry": {
|
|
"domain": registry["work_entry_domain"],
|
|
"channel": registry["work_entry_channel"],
|
|
},
|
|
"repository_binding": {
|
|
"host": "guanghu.chat",
|
|
"username": human["username"],
|
|
"repository": human["repository"],
|
|
"private": True,
|
|
},
|
|
"persona_relationships": human["personas"],
|
|
"persona_identity_governance": registry["persona_identity_governance"],
|
|
"registry_version": registry["version"],
|
|
}
|
|
if db is not None:
|
|
relationship = db.execute(
|
|
"SELECT decision,observed_at,receipt_hash FROM relationship_receipts WHERE human_number=? ORDER BY observed_at DESC LIMIT 1",
|
|
(human["human_number"],),
|
|
).fetchone()
|
|
responsibility = db.execute(
|
|
"SELECT decision,observed_at,receipt_hash,responsibility_version FROM responsibility_receipts WHERE human_number=? ORDER BY observed_at DESC LIMIT 1",
|
|
(human["human_number"],),
|
|
).fetchone()
|
|
projection["relationship_confirmation"] = dict(relationship) if relationship else None
|
|
projection["responsibility_receipt"] = dict(responsibility) if responsibility else None
|
|
return projection
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
server_version = "GuanghuEnterpriseIdentity/1.0"
|
|
|
|
def log_message(self, fmt: str, *args: object) -> None:
|
|
# Never include headers or request bodies in logs.
|
|
print("[enterprise-identity] " + fmt % args)
|
|
|
|
def respond(self, status: int, body: dict) -> None:
|
|
encoded = json.dumps(body, ensure_ascii=False).encode()
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
self.send_header("Content-Length", str(len(encoded)))
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.send_header("X-Content-Type-Options", "nosniff")
|
|
self.end_headers()
|
|
self.wfile.write(encoded)
|
|
|
|
def body(self) -> dict:
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
if length < 1 or length > MAX_BODY:
|
|
raise ValueError("request body size invalid")
|
|
value = json.loads(self.rfile.read(length).decode())
|
|
if not isinstance(value, dict):
|
|
raise ValueError("JSON object required")
|
|
return value
|
|
|
|
def authenticated_human(self, registry: dict, payload: dict) -> tuple[dict, str, str] | None:
|
|
credentials = parse_basic(self.headers.get("Authorization", ""))
|
|
number = str(payload.get("human_number", ""))
|
|
human = find_human(registry, number)
|
|
if not credentials or not human:
|
|
return None
|
|
username, password = credentials
|
|
if not hmac.compare_digest(username, human["username"]) or not verify_forgejo(username, password):
|
|
return None
|
|
return human, username, password
|
|
|
|
def do_GET(self) -> None:
|
|
try:
|
|
registry = load_registry()
|
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
return self.respond(503, {"ok": False, "error": f"registry unavailable: {error}"})
|
|
parsed = urllib.parse.urlparse(self.path)
|
|
if parsed.path == "/health":
|
|
return self.respond(200, {"ok": True, "service": "guanghu-enterprise-identity", "registry_version": registry["version"], "age_species": "AGE", "valid_age_individual_numbers": 0})
|
|
if parsed.path == "/v1/resolve":
|
|
number = urllib.parse.parse_qs(parsed.query).get("id", [""])[0]
|
|
human = find_human(registry, number)
|
|
return self.respond(200 if human else 404, public_projection(registry, human) if human else {"status": "NOT_FOUND"})
|
|
return self.respond(404, {"ok": False, "error": "not found"})
|
|
|
|
def do_POST(self) -> None:
|
|
try:
|
|
registry = load_registry()
|
|
payload = self.body()
|
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
return self.respond(400, {"ok": False, "error": str(error)})
|
|
if self.path == "/v1/change-password":
|
|
credentials = parse_basic(self.headers.get("Authorization", ""))
|
|
human = find_human(registry, str(payload.get("human_number", "")))
|
|
new_password = str(payload.get("new_password", ""))
|
|
if not credentials or not human:
|
|
return self.respond(401, {"ok": False, "error": "enterprise account authentication failed"})
|
|
username, current_password = credentials
|
|
if not hmac.compare_digest(username, human["username"]):
|
|
return self.respond(401, {"ok": False, "error": "enterprise account authentication failed"})
|
|
if (
|
|
len(new_password) < 14
|
|
or len(new_password) > 128
|
|
or hmac.compare_digest(current_password, new_password)
|
|
or new_password.isdigit()
|
|
or new_password.isalpha()
|
|
):
|
|
return self.respond(400, {"ok": False, "error": "new password does not meet the first-login policy"})
|
|
if not rotate_forgejo_password(username, current_password, new_password):
|
|
return self.respond(401, {"ok": False, "error": "first-login password rotation failed"})
|
|
observed = now()
|
|
receipt_id = "GH-CRED-" + uuid.uuid4().hex.upper()
|
|
receipt = signed_receipt({
|
|
"receipt_id": receipt_id,
|
|
"human_number": human["human_number"],
|
|
"username": username,
|
|
"observed_at": observed,
|
|
"password_changed": True,
|
|
})
|
|
db = database()
|
|
try:
|
|
db.execute(
|
|
"INSERT INTO credential_rotation_receipts VALUES (?,?,?,?,?,?)",
|
|
(receipt_id, human["human_number"], username, observed, receipt["receipt_hash"], receipt["receipt_signature"]),
|
|
)
|
|
db.execute(
|
|
"INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)",
|
|
(observed, "CREDENTIAL_ROTATION", hashlib.sha256(human["human_number"].encode()).hexdigest(), receipt_id),
|
|
)
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
return self.respond(200, {"ok": True, "receipt": receipt})
|
|
authenticated = self.authenticated_human(registry, payload)
|
|
if not authenticated:
|
|
return self.respond(401, {"ok": False, "error": "enterprise account authentication failed"})
|
|
human, username, password = authenticated
|
|
idempotency_key = str(payload.get("idempotency_key", ""))
|
|
if not re.fullmatch(r"[A-Za-z0-9_-]{16,96}", idempotency_key):
|
|
return self.respond(400, {"ok": False, "error": "valid idempotency_key required"})
|
|
db = database()
|
|
try:
|
|
if self.path == "/v1/relationship-confirmations":
|
|
decision = str(payload.get("decision", ""))
|
|
if decision not in {"CONFIRM", "REJECT"}:
|
|
return self.respond(400, {"ok": False, "error": "relationship decision invalid"})
|
|
existing = db.execute("SELECT * FROM relationship_receipts WHERE idempotency_key=?", (idempotency_key,)).fetchone()
|
|
if existing:
|
|
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
|
|
observed = now()
|
|
receipt_id = stable_receipt_id("GH-REL", human["human_number"], idempotency_key)
|
|
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"registry_version":registry["version"],"decision":decision,"observed_at":observed})
|
|
try:
|
|
projection = project_receipt_to_repository(human, username, password, "relationship", receipt)
|
|
except RuntimeError as error:
|
|
return self.respond(503, {"ok": False, "error": str(error)})
|
|
db.execute("INSERT INTO relationship_receipts VALUES (?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,registry["version"],decision,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
|
|
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RELATIONSHIP_CONFIRMATION",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
|
|
db.commit()
|
|
return self.respond(201, {"ok": True, "receipt": receipt, "repository_projection": projection})
|
|
if self.path == "/v1/responsibility-receipts":
|
|
decision = str(payload.get("decision", ""))
|
|
note = str(payload.get("note", ""))[:1000]
|
|
version = str(payload.get("responsibility_version", ""))
|
|
if decision not in DECISIONS or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}\.[0-9]+", version):
|
|
return self.respond(400, {"ok": False, "error": "responsibility decision or version invalid"})
|
|
existing = db.execute("SELECT * FROM responsibility_receipts WHERE idempotency_key=?", (idempotency_key,)).fetchone()
|
|
if existing:
|
|
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
|
|
observed = now()
|
|
receipt_id = stable_receipt_id("GH-RESP", human["human_number"], idempotency_key)
|
|
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"responsibility_domain":human["responsibility_domain"],"responsibility_version":version,"decision":decision,"note":note,"observed_at":observed})
|
|
try:
|
|
projection = project_receipt_to_repository(human, username, password, "responsibility", receipt)
|
|
except RuntimeError as error:
|
|
return self.respond(503, {"ok": False, "error": str(error)})
|
|
db.execute("INSERT INTO responsibility_receipts VALUES (?,?,?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,human["responsibility_domain"],version,decision,note,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
|
|
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RESPONSIBILITY_RECEIPT",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
|
|
db.commit()
|
|
return self.respond(201, {"ok": True, "receipt": receipt, "repository_projection": projection})
|
|
if self.path == "/v1/me/entry":
|
|
return self.respond(200, {"ok": True, "entry": public_projection(registry, human, db)})
|
|
return self.respond(404, {"ok": False, "error": "not found"})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
load_registry()
|
|
if len(RECEIPT_KEY) < 32:
|
|
raise SystemExit("GH_ENTERPRISE_RECEIPT_KEY must contain at least 32 characters")
|
|
ThreadingHTTPServer((BIND, PORT), Handler).serve_forever()
|