83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
"""Authentik-shell payload for an idempotent MCP service account and app password.
|
|
|
|
Run by piping this file to ``ak shell`` with MCP_USERNAME, MCP_DISPLAY_NAME,
|
|
MCP_TOKEN_IDENTIFIER and MCP_SECRET_FILE set inside the container. The secret
|
|
is read from a temporary file and is never printed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from authentik.core.models import Token, TokenIntents, User, UserTypes
|
|
from django.db import transaction
|
|
|
|
|
|
def required(name: str) -> str:
|
|
value = os.environ.get(name, "").strip()
|
|
if not value:
|
|
raise ValueError(f"missing {name}")
|
|
return value
|
|
|
|
|
|
username = required("MCP_USERNAME")
|
|
display_name = required("MCP_DISPLAY_NAME")
|
|
identifier = required("MCP_TOKEN_IDENTIFIER")
|
|
secret_path = Path(required("MCP_SECRET_FILE"))
|
|
if secret_path.is_symlink() or not secret_path.is_file() or secret_path.stat().st_size > 8192:
|
|
raise ValueError("invalid app-password secret file")
|
|
secret = secret_path.read_text(encoding="utf-8").strip()
|
|
if not secret or "\n" in secret or "\r" in secret:
|
|
raise ValueError("app-password secret must contain one non-empty line")
|
|
|
|
with transaction.atomic():
|
|
user, created = User.objects.get_or_create(
|
|
username=username,
|
|
defaults={
|
|
"name": display_name,
|
|
"path": "users",
|
|
"type": UserTypes.SERVICE_ACCOUNT,
|
|
},
|
|
)
|
|
if not created and user.type not in {
|
|
UserTypes.SERVICE_ACCOUNT,
|
|
UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
|
}:
|
|
raise ValueError("existing Authentik identity is not a service account")
|
|
if user.name != display_name:
|
|
user.name = display_name
|
|
user.save(update_fields=["name"])
|
|
token, token_created = Token.objects.get_or_create(
|
|
identifier=identifier,
|
|
defaults={
|
|
"user": user,
|
|
"intent": TokenIntents.INTENT_APP_PASSWORD,
|
|
"key": secret,
|
|
"expiring": False,
|
|
"description": "ITWorx MCP Hub Codex machine credential",
|
|
},
|
|
)
|
|
if not token_created:
|
|
if token.user_id != user.pk or token.intent != TokenIntents.INTENT_APP_PASSWORD:
|
|
raise ValueError("existing Authentik token identifier has incompatible ownership")
|
|
if os.environ.get("MCP_ROTATE", "false").lower() == "true":
|
|
token.key = secret
|
|
token.expiring = False
|
|
token.save(update_fields=["key", "expiring"])
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"created": created,
|
|
"tokenCreated": token_created,
|
|
"subject": str(user.uuid),
|
|
"username": user.username,
|
|
"tokenIdentifier": token.identifier,
|
|
},
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
del secret
|