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.
221 lines
7.0 KiB
Python
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
|