my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
at main 5.8 kB view raw
1/** 2 * LDAP Orphan Account Audit Script 3 * 4 * This script identifies Indiko accounts provisioned via LDAP that no longer exist in LDAP. 5 * Useful for detecting when users have been removed from LDAP but their Indiko accounts remain active. 6 * 7 * Usage: bun scripts/audit-ldap-orphans.ts [--suspend | --deactivate | --dry-run] 8 * 9 * Flags: 10 * --dry-run Show what would be done without making changes (default) 11 * --suspend Set status to 'suspended' for orphaned accounts 12 * --deactivate Set status to 'inactive' for orphaned accounts 13 */ 14 15import { Database } from "bun:sqlite"; 16import * as path from "node:path"; 17import { authenticate } from "ldap-authentication"; 18 19// Load database 20const dbPath = path.join(import.meta.dir, "..", "data", "indiko.db"); 21const db = new Database(dbPath); 22 23// Configuration from environment 24const LDAP_URL = process.env.LDAP_URL || "ldap://localhost:389"; 25const LDAP_ADMIN_DN = process.env.LDAP_ADMIN_DN; 26const LDAP_ADMIN_PASSWORD = process.env.LDAP_ADMIN_PASSWORD; 27const LDAP_USER_SEARCH_BASE = 28 process.env.LDAP_USER_SEARCH_BASE || "dc=example,dc=com"; 29const LDAP_USERNAME_ATTRIBUTE = process.env.LDAP_USERNAME_ATTRIBUTE || "uid"; 30 31interface LdapUser { 32 username: string; 33 id: number; 34 status: string; 35 created_at: number; 36} 37 38interface AuditResult { 39 total: number; 40 active: number; 41 orphaned: number; 42 errors: number; 43 orphanedUsers: Array<{ 44 username: string; 45 id: number; 46 status: string; 47 createdAt: number; 48 }>; 49} 50 51async function checkLdapUser(username: string): Promise<boolean> { 52 try { 53 const user = await authenticate({ 54 ldapOpts: { 55 url: LDAP_URL, 56 }, 57 adminDn: LDAP_ADMIN_DN, 58 adminPassword: LDAP_ADMIN_PASSWORD, 59 userSearchBase: LDAP_USER_SEARCH_BASE, 60 usernameAttribute: LDAP_USERNAME_ATTRIBUTE, 61 username: username, 62 verifyUserExists: true, 63 }); 64 return !!user; 65 } catch (error) { 66 // User not found or invalid credentials (expected for non-existence check) 67 return false; 68 } 69} 70 71async function auditLdapAccounts(): Promise<AuditResult> { 72 console.log("🔍 Starting LDAP orphan account audit...\n"); 73 74 // Get all LDAP-provisioned users 75 const ldapUsers = db 76 .query( 77 "SELECT id, username, status, created_at FROM users WHERE provisioned_via_ldap = 1", 78 ) 79 .all() as LdapUser[]; 80 81 const result: AuditResult = { 82 total: ldapUsers.length, 83 active: 0, 84 orphaned: 0, 85 errors: 0, 86 orphanedUsers: [], 87 }; 88 89 console.log(`Found ${result.total} LDAP-provisioned accounts\n`); 90 91 // Check each user against LDAP 92 for (const user of ldapUsers) { 93 process.stdout.write(`Checking ${user.username}... `); 94 95 try { 96 const existsInLdap = await checkLdapUser(user.username); 97 98 if (existsInLdap) { 99 console.log("✅ Found in LDAP"); 100 result.active++; 101 } else { 102 console.log("❌ NOT FOUND in LDAP"); 103 result.orphaned++; 104 result.orphanedUsers.push({ 105 username: user.username, 106 id: user.id, 107 status: user.status, 108 createdAt: user.created_at, 109 }); 110 } 111 } catch (error) { 112 console.log("⚠️ Error checking LDAP"); 113 result.errors++; 114 console.error( 115 ` Error: ${error instanceof Error ? error.message : String(error)}`, 116 ); 117 } 118 } 119 120 return result; 121} 122 123function printReport(result: AuditResult): void { 124 console.log(`\n${"=".repeat(60)}`); 125 console.log("LDAP ORPHAN ACCOUNT AUDIT REPORT"); 126 console.log(`${"=".repeat(60)}\n`); 127 128 console.log(`Total LDAP-provisioned accounts: ${result.total}`); 129 console.log(`Active in LDAP: ${result.active}`); 130 console.log(`Orphaned (missing from LDAP): ${result.orphaned}`); 131 console.log(`Check errors: ${result.errors}`); 132 133 if (result.orphaned === 0) { 134 console.log("\n✅ No orphaned accounts found!"); 135 return; 136 } 137 138 console.log(`\n${"-".repeat(60)}`); 139 console.log("ORPHANED ACCOUNTS:"); 140 console.log(`${"-".repeat(60)}\n`); 141 142 result.orphanedUsers.forEach((user, idx) => { 143 console.log(`${idx + 1}. ${user.username}`); 144 console.log( 145 ` ID: ${user.id} | Status: ${user.status} | Created: ${new Date(user.createdAt * 1000).toISOString().split("T")[0]}`, 146 ); 147 }); 148} 149 150async function updateOrphanedAccounts( 151 result: AuditResult, 152 action: "suspend" | "deactivate", 153): Promise<void> { 154 const newStatus = action === "suspend" ? "suspended" : "inactive"; 155 156 console.log( 157 `\n📝 Updating ${result.orphaned} orphaned account(s) to status: '${newStatus}'`, 158 ); 159 160 for (const user of result.orphanedUsers) { 161 db.query("UPDATE users SET status = ? WHERE id = ?").run( 162 newStatus, 163 user.id, 164 ); 165 console.log(` Updated: ${user.username}`); 166 } 167 168 console.log(`\n✅ Updated ${result.orphaned} account(s)`); 169} 170 171async function main() { 172 // Validate LDAP configuration 173 if (!LDAP_ADMIN_DN || !LDAP_ADMIN_PASSWORD) { 174 console.error( 175 "❌ Error: LDAP_ADMIN_DN and LDAP_ADMIN_PASSWORD environment variables are required", 176 ); 177 process.exit(1); 178 } 179 180 const args = process.argv.slice(2); 181 const dryRun = args.includes("--dry-run") || args.length === 0; 182 const shouldSuspend = args.includes("--suspend"); 183 const shouldDeactivate = args.includes("--deactivate"); 184 185 if (dryRun) { 186 console.log("🔄 Running in DRY-RUN mode (no changes will be made)\n"); 187 } 188 189 try { 190 const result = await auditLdapAccounts(); 191 printReport(result); 192 193 if (!dryRun && result.orphaned > 0) { 194 if (shouldSuspend) { 195 await updateOrphanedAccounts(result, "suspend"); 196 } else if (shouldDeactivate) { 197 await updateOrphanedAccounts(result, "deactivate"); 198 } else { 199 console.log( 200 "\n⚠️ No action specified. Use --suspend or --deactivate to update accounts.", 201 ); 202 } 203 } 204 205 process.exit(0); 206 } catch (error) { 207 console.error( 208 "\n❌ Audit failed:", 209 error instanceof Error ? error.message : String(error), 210 ); 211 process.exit(1); 212 } 213} 214 215main();