233 lines
10 KiB
PHP
233 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Office Admin Recovery API V96.23
|
|
*
|
|
* User: "CHECK OFFICEAPP ET OFFICE WORKFLOW POUR CREER DES RECOVERY ADMIN ACCOUNTS POUR LES OFFICE"
|
|
*
|
|
* Audit revealed:
|
|
* - 965 accounts in admin.office_accounts (854 active + 111 warming)
|
|
* - Only 8/965 have backdoor_email configured (0.8% coverage 🔴)
|
|
* - Table already has fields: backdoor_email, backdoor_password, backdoor_created, recovery_attempts
|
|
* - Goal: generate plan to create recovery admin for EVERY tenant (9 tenants)
|
|
*
|
|
* Actions:
|
|
* - action=audit : coverage report by tenant
|
|
* - action=plan : generate backdoor creation plan (priority ordered)
|
|
* - action=gaps : list accounts missing backdoor
|
|
* - action=create_plan : suggest backdoor emails + store in DB (idempotent)
|
|
*/
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
try {
|
|
$db = new PDO("pgsql:host=10.1.0.3;port=5432;dbname=adx_system", "admin", "admin123",
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
|
$db->exec("SET search_path TO admin,public");
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(["ok" => false, "error" => "db: " . $e->getMessage()]);
|
|
exit;
|
|
}
|
|
|
|
$action = $_GET['action'] ?? $_POST['action'] ?? 'audit';
|
|
|
|
// ═══ ACTION 1: AUDIT — coverage report ═══
|
|
if ($action === 'audit') {
|
|
$rows = $db->query("
|
|
SELECT
|
|
tenant_domain,
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER(WHERE backdoor_email IS NOT NULL AND backdoor_email != '') as has_backdoor,
|
|
COUNT(*) FILTER(WHERE admin_email IS NOT NULL AND admin_email != '') as has_admin,
|
|
COUNT(*) FILTER(WHERE LOWER(status) = 'suspended') as suspended,
|
|
COUNT(*) FILTER(WHERE LOWER(status) = 'active') as active,
|
|
COUNT(*) FILTER(WHERE recovery_attempts > 0) as recovery_tried
|
|
FROM admin.office_accounts
|
|
GROUP BY tenant_domain
|
|
HAVING COUNT(*) FILTER(WHERE app_id IS NOT NULL AND app_id != '') > 0
|
|
ORDER BY total DESC
|
|
")->fetchAll();
|
|
|
|
$total_accounts = 0; $total_backdoor = 0;
|
|
foreach ($rows as &$r) {
|
|
$total_accounts += $r['total'];
|
|
$total_backdoor += $r['has_backdoor'];
|
|
$r['coverage_pct'] = $r['total'] > 0 ? round($r['has_backdoor'] / $r['total'] * 100, 1) : 0;
|
|
$r['risk'] = ($r['coverage_pct'] < 20) ? 'HIGH' : (($r['coverage_pct'] < 50) ? 'MEDIUM' : 'LOW');
|
|
}
|
|
|
|
$global_cov = $total_accounts > 0 ? round($total_backdoor / $total_accounts * 100, 2) : 0;
|
|
|
|
echo json_encode([
|
|
"ok" => true,
|
|
"v" => "V96.23-office-recovery-audit-opus",
|
|
"ts" => date('c'),
|
|
"summary" => [
|
|
"total_accounts" => (int)$total_accounts,
|
|
"has_backdoor" => (int)$total_backdoor,
|
|
"global_coverage_pct" => $global_cov,
|
|
"tenants_count" => count($rows),
|
|
"verdict" => $global_cov < 20 ? "🔴 CRITICAL - Quasi aucun admin recovery" : ($global_cov < 50 ? "⚠️ WARNING" : "✅ GOOD"),
|
|
],
|
|
"by_tenant" => $rows,
|
|
"recommendations" => [
|
|
"immediate" => "Create backdoor admin on top 3 tenants (accoff04/06/02) — highest user count",
|
|
"strategy" => "1 backdoor per tenant minimum + 1 backup at org level",
|
|
"security" => "Store credentials in /opt/wevads/vault (chattr +i) · rotation 90 days",
|
|
],
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// ═══ ACTION 2: GAPS — list accounts missing backdoor ═══
|
|
if ($action === 'gaps') {
|
|
$limit = min(200, max(10, (int)($_GET['limit'] ?? 50)));
|
|
$rows = $db->query("
|
|
SELECT id, name, tenant_domain, admin_email, status, has_license, has_mfa,
|
|
backdoor_email, recovery_attempts, last_recovery_attempt
|
|
FROM admin.office_accounts
|
|
WHERE (backdoor_email IS NULL OR backdoor_email = '')
|
|
AND LOWER(status) = 'active'
|
|
ORDER BY tenant_domain, id
|
|
LIMIT $limit
|
|
")->fetchAll();
|
|
|
|
echo json_encode([
|
|
"ok" => true,
|
|
"v" => "V96.23-gaps",
|
|
"count" => count($rows),
|
|
"limit" => $limit,
|
|
"accounts_missing_backdoor" => $rows,
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// ═══ ACTION 3: PLAN — generate backdoor creation plan ═══
|
|
if ($action === 'plan') {
|
|
$tenants = $db->query("
|
|
SELECT
|
|
tenant_domain,
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER(WHERE backdoor_email IS NOT NULL AND backdoor_email != '') as has_backdoor,
|
|
MIN(admin_email) FILTER(WHERE admin_email IS NOT NULL AND admin_email != '') as sample_admin
|
|
FROM admin.office_accounts
|
|
GROUP BY tenant_domain
|
|
HAVING COUNT(*) FILTER(WHERE app_id IS NOT NULL AND app_id != '') > 0
|
|
ORDER BY total DESC
|
|
")->fetchAll();
|
|
|
|
$plan = [];
|
|
$priority_counter = 1;
|
|
foreach ($tenants as $t) {
|
|
$domain = $t['tenant_domain'] ?? 'unknown';
|
|
$coverage = $t['total'] > 0 ? round($t['has_backdoor'] / $t['total'] * 100, 1) : 0;
|
|
|
|
if ($coverage >= 100) continue; // skip fully covered
|
|
|
|
// Suggested backdoor naming convention
|
|
$suggested_upn = "recovery-admin-" . substr(md5($domain), 0, 6) . "@" . $domain;
|
|
$suggested_name = "WEVAL Recovery Admin " . date('Y-m');
|
|
|
|
$plan[] = [
|
|
"priority" => $priority_counter++,
|
|
"tenant_domain" => $domain,
|
|
"current_total" => (int)$t['total'],
|
|
"current_backdoors" => (int)$t['has_backdoor'],
|
|
"missing_backdoors" => 1 - min(1, (int)$t['has_backdoor']), // at least 1 per tenant
|
|
"urgency" => $coverage < 20 ? "CRITICAL" : ($coverage < 50 ? "HIGH" : "MEDIUM"),
|
|
"suggested_backdoor" => [
|
|
"upn" => $suggested_upn,
|
|
"display_name" => $suggested_name,
|
|
"role" => "Global Administrator",
|
|
"license" => "Office 365 E3 (or existing spare)",
|
|
"mfa" => "REQUIRED (authenticator app)",
|
|
],
|
|
"creation_steps" => [
|
|
"1. Portal: https://admin.microsoft.com (login with existing admin: " . ($t['sample_admin'] ?? 'N/A') . ")",
|
|
"2. Users → Active users → Add a user",
|
|
"3. Fill: Name='WEVAL Recovery', Username='recovery-admin-" . substr(md5($domain), 0, 6) . "'",
|
|
"4. Roles: assign 'Global administrator'",
|
|
"5. Password: generate strong + store in vault",
|
|
"6. Enable MFA via authenticator",
|
|
"7. Test login + immediately store credentials in /opt/wevads/vault encrypted",
|
|
"8. UPDATE admin.office_accounts SET backdoor_email='...', backdoor_password='...', backdoor_created=NOW() WHERE tenant_domain='$domain'",
|
|
],
|
|
"estimated_time_minutes" => 5,
|
|
];
|
|
}
|
|
|
|
echo json_encode([
|
|
"ok" => true,
|
|
"v" => "V96.23-plan",
|
|
"ts" => date('c'),
|
|
"doctrine" => "#4 HONNETE: steps explicit, no auto-create (requires admin console login Yacine-only)",
|
|
"total_tenants_need_backdoor" => count($plan),
|
|
"total_time_estimated_minutes" => count($plan) * 5,
|
|
"plan" => $plan,
|
|
"next_action" => "POST /api/office-recovery.php action=register_backdoor with {tenant_domain, upn, password, tenant_id} after manual creation in admin console",
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// ═══ ACTION 4: REGISTER_BACKDOOR — store credentials after manual creation ═══
|
|
if ($action === 'register_backdoor' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$body = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
|
$required = ['tenant_domain', 'backdoor_email', 'backdoor_password'];
|
|
foreach ($required as $k) {
|
|
if (empty($body[$k])) {
|
|
http_response_code(400);
|
|
echo json_encode(["ok" => false, "error" => "missing $k"]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
$tenant = $body['tenant_domain'];
|
|
$email = $body['backdoor_email'];
|
|
$pwd_hash = password_hash($body['backdoor_password'], PASSWORD_BCRYPT); // store hash, not plain
|
|
|
|
// Update the first active account as bookkeeper for this tenant
|
|
$stmt = $db->prepare("
|
|
UPDATE admin.office_accounts
|
|
SET backdoor_email = :email,
|
|
backdoor_password = :pwd,
|
|
backdoor_created = NOW()
|
|
WHERE tenant_domain = :domain
|
|
AND LOWER(status) = 'active'
|
|
AND (backdoor_email IS NULL OR backdoor_email = '')
|
|
ORDER BY id
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':email' => $email, ':pwd' => $pwd_hash, ':domain' => $tenant]);
|
|
$affected = $stmt->rowCount();
|
|
|
|
// Log the event
|
|
@file_put_contents('/var/log/weval/office-recovery.log',
|
|
sprintf("[%s] register_backdoor tenant=%s email=%s affected=%d\n",
|
|
date('c'), $tenant, $email, $affected),
|
|
FILE_APPEND);
|
|
|
|
echo json_encode([
|
|
"ok" => $affected > 0,
|
|
"v" => "V96.23-register",
|
|
"tenant_domain" => $tenant,
|
|
"backdoor_email" => $email,
|
|
"accounts_updated" => $affected,
|
|
"note" => "Password stored as bcrypt hash. Plain password should be stored in /opt/wevads/vault manually.",
|
|
"ts" => date('c'),
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// Default: help
|
|
echo json_encode([
|
|
"ok" => true,
|
|
"v" => "V96.23-office-recovery-opus",
|
|
"actions" => [
|
|
"audit" => "GET ?action=audit — coverage report by tenant",
|
|
"gaps" => "GET ?action=gaps&limit=50 — list accounts missing backdoor",
|
|
"plan" => "GET ?action=plan — generate creation plan per tenant (priority ordered)",
|
|
"register_backdoor" => "POST ?action=register_backdoor — store credentials after manual Azure admin create",
|
|
],
|
|
"philosophy" => "Backdoor creation requires admin console login (Yacine-only per secrets.env constraints). This API generates the PLAN, tracks progress, and secures stored credentials.",
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|