Files
apim-apps/apim_apps/engine.py
T
k3nny 0f1a6d865d feat(apim-apps): add external credentials support for new applications
Applications can now declare a `credentials:` list in the YAML manifest.
Each entry holds a static `client_id` (and optional `secret`) that is
provisioned via POST /applications/{id}/extcredentials when the application
is first created. Existing applications are never modified.

Also adds CHANGELOG.md and ROADMAP.md for the v0.1.0 initial release.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 22:16:48 +02:00

184 lines
7.7 KiB
Python

import logging
from ._colors import _c
from .client import AxwayClient
from .models import Action, EnvConfig, Manifest, NormResult
log = logging.getLogger(__name__)
def _applies_to(target_envs: list[str], env_name: str) -> bool:
return not target_envs or env_name in target_envs
def normalize_env(env: EnvConfig, manifest: Manifest, dry_run: bool) -> NormResult:
result = NormResult()
tag = f"[{env.name}]"
log.info(f"{tag} Connexion à {env.url} …")
client = AxwayClient(env)
# ── 1. Organisations ──────────────────────────────────────────────────────
try:
existing_orgs = client.list_orgs()
except Exception as e:
result.errors.append(f"{tag} Impossible de lister les organisations : {e}")
return result
for org_spec in manifest.organizations:
if not _applies_to(org_spec.environments, env.name):
continue
action = Action(
kind="create_org",
env_name=env.name,
description=f"Créer organisation « {org_spec.name} »",
detail={"org": org_spec.name},
)
if org_spec.name in existing_orgs:
log.debug(f"{tag} Org « {org_spec.name} » déjà présente — skip")
result.actions_skipped.append(action)
continue
log.info(_c("yellow", f"{tag} {'[DRY-RUN] ' if dry_run else ''}Création org « {org_spec.name} »"))
if not dry_run:
try:
created = client.create_org(org_spec)
existing_orgs[org_spec.name] = created
log.info(_c("green", f"{tag} ✅ Org « {org_spec.name} » créée (id={created.get('id')})"))
except Exception as e:
msg = f"{tag} ❌ Échec création org « {org_spec.name} » : {e}"
log.error(msg)
result.errors.append(msg)
continue
else:
# Placeholder so apps referencing this org are processed in dry-run
existing_orgs[org_spec.name] = {"id": "", "name": org_spec.name}
result.actions_done.append(action)
# ── 2. Applications ───────────────────────────────────────────────────────
try:
existing_apps = client.list_apps()
except Exception as e:
result.errors.append(f"{tag} Impossible de lister les applications : {e}")
return result
for app_spec in manifest.applications:
if not _applies_to(app_spec.environments, env.name):
continue
key = (app_spec.org_name, app_spec.name)
action_app = Action(
kind="create_app",
env_name=env.name,
description=f"Créer application « {app_spec.name} » dans org « {app_spec.org_name} »",
detail={"org": app_spec.org_name, "app": app_spec.name},
)
raw_org = existing_orgs.get(app_spec.org_name)
if raw_org is None:
msg = (
f"{tag} ❌ Organisation « {app_spec.org_name} » introuvable dans l'API Manager "
f"— application « {app_spec.name} » ignorée. "
f"Créez l'organisation manuellement dans l'API Manager."
)
log.error(msg)
result.errors.append(msg)
continue
org_id = raw_org["id"]
if key not in existing_apps:
log.info(_c("yellow", f"{tag} {'[DRY-RUN] ' if dry_run else ''}Création app « {app_spec.name} » (org={app_spec.org_name})"))
if not dry_run:
try:
created_app = client.create_app(app_spec, org_id)
existing_apps[key] = created_app
log.info(_c("green", f"{tag} ✅ App « {app_spec.name} » créée (id={created_app.get('id')})"))
except Exception as e:
msg = f"{tag} ❌ Échec création app « {app_spec.name} » : {e}"
log.error(msg)
result.errors.append(msg)
continue
app_id_new = created_app.get("id", "")
for cred in app_spec.credentials:
try:
client.create_app_ext_credential(app_id_new, cred.client_id, cred.secret)
log.info(_c("green", f"{tag} ✅ External credential « {cred.client_id} » ajouté à « {app_spec.name} »"))
except Exception as e:
msg = f"{tag} ❌ Échec ajout external credential à « {app_spec.name} » : {e}"
log.error(msg)
result.errors.append(msg)
result.actions_done.append(action_app)
else:
result.actions_skipped.append(action_app)
# ── 3. Souscriptions aux APIs ─────────────────────────────────────────
if not app_spec.apis:
continue
raw_app = existing_apps.get(key)
if raw_app is None:
continue # app non créée (dry-run ou erreur)
app_id = raw_app.get("id", "")
try:
granted = client.list_app_api_access(app_id)
granted_ids = {a.get("apiId", a.get("id", "")) for a in granted}
granted_names = {a.get("apiName", a.get("name", "")) for a in granted}
except Exception as e:
log.warning(f"{tag} Impossible de lire les accès API de « {app_spec.name} » : {e}")
granted_ids = set()
granted_names = set()
for api_access in app_spec.apis:
action_api = Action(
kind="grant_api",
env_name=env.name,
description=(
f"Abonner « {app_spec.name} » à l'API « {api_access.api_name} »"
+ (f" v{api_access.api_version}" if api_access.api_version else "")
),
detail={"app": app_spec.name, "api": api_access.api_name, "version": api_access.api_version},
)
if api_access.api_name in granted_names:
log.debug(f"{tag} API « {api_access.api_name} » déjà accordée à « {app_spec.name} » — skip")
result.actions_skipped.append(action_api)
continue
if dry_run:
log.info(_c("yellow", f"{tag} [DRY-RUN] Abonnement « {app_spec.name} » → API « {api_access.api_name} »"))
result.actions_done.append(action_api)
continue
fe_api = client.find_frontend_api(api_access.api_name, api_access.api_version)
if fe_api is None:
msg = (
f"{tag} ⚠️ API front-end « {api_access.api_name} » introuvable "
f"— souscription de « {app_spec.name} » ignorée"
)
log.warning(msg)
result.errors.append(msg)
continue
api_id = fe_api["id"]
if api_id in granted_ids:
result.actions_skipped.append(action_api)
continue
try:
client.grant_api_access(app_id, api_id)
log.info(_c("green", f"{tag} ✅ Accès API « {api_access.api_name} » accordé à « {app_spec.name} »"))
result.actions_done.append(action_api)
except Exception as e:
msg = f"{tag} ❌ Échec souscription « {app_spec.name} » → « {api_access.api_name} » : {e}"
log.error(msg)
result.errors.append(msg)
return result