Files
Orchestrator/shared/AI-Orchestrator/scripts/pat_manager/registry.py
ankn a5f8fb49ab Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
2026-06-30 20:39:52 +02:00

221 lines
7.0 KiB
Python

"""PAT Registry module for loading, validating, and persisting the PAT registry."""
from __future__ import annotations
import json
import re
from pathlib import Path
from .errors import RegistryValidationError
from .models import PatEntry
# Valid service names and their required renewal methods
VALID_SERVICES: dict[str, str] = {
"GitLab": "auto",
"Jira": "manual",
"Confluence": "manual",
"OrgMyLife": "manual",
}
# ISO 8601 date pattern (YYYY-MM-DD)
_ISO_DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
# Maximum token name length
_MAX_TOKEN_NAME_LENGTH = 128
class PatRegistry:
"""Manages loading, validation, and persistence of the PAT registry.
The registry is a JSON file with structure:
{"version": "1.0", "tokens": [...]}
Each token entry contains: service, token_name, expiry_date, renewal_method.
"""
def load(self, path: Path) -> list[PatEntry]:
"""Load and validate all entries from a PAT registry JSON file.
Args:
path: Path to the registry JSON file.
Returns:
List of validated PatEntry objects.
Raises:
FileNotFoundError: If the registry file does not exist.
json.JSONDecodeError: If the file contains invalid JSON.
RegistryValidationError: If any entry fails validation.
"""
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise RegistryValidationError(
entry={},
failed_fields=["root"],
)
tokens = data.get("tokens", [])
if not isinstance(tokens, list):
raise RegistryValidationError(
entry={},
failed_fields=["tokens"],
)
entries: list[PatEntry] = []
seen: set[tuple[str, str]] = set()
for raw_entry in tokens:
entry = self.validate_entry(raw_entry)
# Check for duplicates
key = (entry.service, entry.token_name)
if key in seen:
raise RegistryValidationError(
entry=raw_entry,
failed_fields=["service", "token_name"],
)
seen.add(key)
entries.append(entry)
return entries
def validate_entry(self, entry: dict) -> PatEntry:
"""Validate a single registry entry dictionary.
Checks:
- All required fields are present (service, token_name, renewal_method)
- service is one of the supported values
- token_name is non-empty and max 128 characters
- expiry_date is null or valid ISO 8601 (YYYY-MM-DD)
- renewal_method matches the service (GitLab -> "auto", others -> "manual")
Args:
entry: Raw dictionary representing a PAT entry.
Returns:
A validated PatEntry instance.
Raises:
RegistryValidationError: If validation fails, with specific failed fields.
"""
if not isinstance(entry, dict):
raise RegistryValidationError(
entry=entry if isinstance(entry, dict) else {"_raw": str(entry)},
failed_fields=["entry_type"],
)
failed_fields: list[str] = []
# Check required fields presence
required_fields = ["service", "token_name", "renewal_method"]
for field in required_fields:
if field not in entry:
failed_fields.append(field)
# If required fields are missing, raise immediately
if failed_fields:
raise RegistryValidationError(entry=entry, failed_fields=failed_fields)
# Validate service
service = entry["service"]
if service not in VALID_SERVICES:
failed_fields.append("service")
# Validate token_name
token_name = entry["token_name"]
if (
not isinstance(token_name, str)
or len(token_name) == 0
or len(token_name) > _MAX_TOKEN_NAME_LENGTH
):
failed_fields.append("token_name")
# Validate expiry_date (can be null/None or valid ISO 8601 YYYY-MM-DD)
expiry_date = entry.get("expiry_date")
if expiry_date is not None:
if not isinstance(expiry_date, str) or not _is_valid_iso_date(expiry_date):
failed_fields.append("expiry_date")
# Validate renewal_method matches service
renewal_method = entry["renewal_method"]
if service in VALID_SERVICES:
expected_method = VALID_SERVICES[service]
if renewal_method != expected_method:
failed_fields.append("renewal_method")
elif renewal_method not in ("auto", "manual"):
# If service is invalid, still check renewal_method is at least valid
failed_fields.append("renewal_method")
if failed_fields:
raise RegistryValidationError(entry=entry, failed_fields=failed_fields)
return PatEntry(
service=service,
token_name=token_name,
expiry_date=expiry_date,
renewal_method=renewal_method,
)
def save(self, path: Path, entries: list[PatEntry]) -> None:
"""Persist a list of PAT entries to a registry JSON file.
Args:
path: Path where the registry file should be written.
entries: List of PatEntry objects to serialize.
"""
data = {
"version": "1.0",
"tokens": [
{
"service": entry.service,
"token_name": entry.token_name,
"expiry_date": entry.expiry_date,
"renewal_method": entry.renewal_method,
}
for entry in entries
],
}
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
def create_empty(self, path: Path) -> None:
"""Create a new empty registry file with valid JSON structure.
Args:
path: Path where the empty registry file should be created.
"""
self.save(path, [])
def _is_valid_iso_date(date_str: str) -> bool:
"""Check if a string is a valid ISO 8601 date (YYYY-MM-DD).
Validates both the format and that the date values are actually valid
(e.g., rejects 2025-02-30).
"""
if not _ISO_DATE_PATTERN.match(date_str):
return False
# Validate actual date values
try:
year, month, day = date_str.split("-")
year_int, month_int, day_int = int(year), int(month), int(day)
# Basic range checks
if month_int < 1 or month_int > 12:
return False
if day_int < 1 or day_int > 31:
return False
# Use datetime for full validation (handles leap years, etc.)
import datetime
datetime.date(year_int, month_int, day_int)
return True
except (ValueError, OverflowError):
return False