feat: Duo Security integration — full data sync from Accounts + Admin API
Duo API Client (lib/services/duo-client.ts): - HMAC-SHA1 request signing, GET/POST, automatic pagination - Rate-limit handling (429 + Retry-After), configurable timeout - Accounts API: listAccounts() via POST /accounts/v1/account/list - Admin API: getUsers, getPhones, getGroups, getIntegrations, getAuthLogs - Child account access: parent creds signed against child api_hostname + account_id - Factory helpers: getDuoAccountsClient(), getDuoAdminClient() Database (migration 058): - 6 tables: duo_accounts, duo_users, duo_phones, duo_auth_logs, duo_groups, duo_integrations - All with proper FKs, indexes, JSONB fields for capabilities/location/groups Sync Service (lib/services/duo-sync-service.ts): - syncAll(): accounts → per-child data + auth logs → parent account → company matching - Sequential child processing to respect rate limits - Incremental auth logs (mintime = last synced timestamp, default 30 days) - Company matching: exact → case-insensitive containment (30/32 = 94% matched) - Non-blocking with sync ID tracking API Routes: - POST/GET /api/duo/sync — trigger sync / check status - GET /api/duo/accounts — list all accounts with stats + matched company - GET /api/duo/accounts/[id]/users — users for a specific account - POST /api/openclaw/sync/duo — OpenClaw trigger with API key auth Results: 33 accounts, 832 users, 925 phones, 5927 auth logs, 46 groups, 78 integrations
This commit is contained in:
parent
9a448d111c
commit
a4242b81be
10 changed files with 1565 additions and 0 deletions
121
migrations/058_create_duo_tables.sql
Normal file
121
migrations/058_create_duo_tables.sql
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
-- Migration 058: Create Duo Security tables
|
||||
-- Stores data from both Accounts API (child accounts) and Admin API (users, phones, auth logs, groups, integrations)
|
||||
|
||||
-- ── duo_accounts ─────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS duo_accounts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account_id VARCHAR(20) NOT NULL UNIQUE,
|
||||
name VARCHAR(255),
|
||||
api_hostname VARCHAR(255),
|
||||
autotask_company_id BIGINT REFERENCES companies(id) ON DELETE SET NULL,
|
||||
user_count INTEGER DEFAULT 0,
|
||||
integration_count INTEGER DEFAULT 0,
|
||||
edition VARCHAR(50),
|
||||
is_parent BOOLEAN DEFAULT false,
|
||||
synced_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_accounts_company ON duo_accounts(autotask_company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_accounts_is_parent ON duo_accounts(is_parent);
|
||||
|
||||
-- ── duo_users ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS duo_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id VARCHAR(30) NOT NULL UNIQUE,
|
||||
duo_account_id VARCHAR(20) NOT NULL REFERENCES duo_accounts(account_id) ON DELETE CASCADE,
|
||||
username VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
realname VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
is_enrolled BOOLEAN DEFAULT false,
|
||||
last_login TIMESTAMP WITHOUT TIME ZONE,
|
||||
last_directory_sync TIMESTAMP WITHOUT TIME ZONE,
|
||||
created TIMESTAMP WITHOUT TIME ZONE,
|
||||
notes TEXT,
|
||||
phones_count INTEGER DEFAULT 0,
|
||||
groups JSONB,
|
||||
aliases JSONB,
|
||||
enable_auto_prompt BOOLEAN DEFAULT true,
|
||||
synced_at TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_users_account ON duo_users(duo_account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_users_status ON duo_users(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_users_email ON duo_users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_users_enrolled ON duo_users(is_enrolled);
|
||||
|
||||
-- ── duo_phones ───────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS duo_phones (
|
||||
id SERIAL PRIMARY KEY,
|
||||
phone_id VARCHAR(30) NOT NULL UNIQUE,
|
||||
duo_account_id VARCHAR(20) NOT NULL REFERENCES duo_accounts(account_id) ON DELETE CASCADE,
|
||||
name VARCHAR(255),
|
||||
number VARCHAR(50),
|
||||
type VARCHAR(50),
|
||||
platform VARCHAR(50),
|
||||
model VARCHAR(255),
|
||||
os_version VARCHAR(50),
|
||||
app_version VARCHAR(50),
|
||||
activated BOOLEAN DEFAULT false,
|
||||
last_seen TIMESTAMP WITHOUT TIME ZONE,
|
||||
capabilities JSONB,
|
||||
users JSONB,
|
||||
synced_at TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_phones_account ON duo_phones(duo_account_id);
|
||||
|
||||
-- ── duo_auth_logs ────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS duo_auth_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
txid VARCHAR(50) NOT NULL UNIQUE,
|
||||
duo_account_id VARCHAR(20) NOT NULL REFERENCES duo_accounts(account_id) ON DELETE CASCADE,
|
||||
timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
user_name VARCHAR(255),
|
||||
user_id VARCHAR(30),
|
||||
factor VARCHAR(50),
|
||||
result VARCHAR(50),
|
||||
reason VARCHAR(255),
|
||||
application_name VARCHAR(255),
|
||||
application_key VARCHAR(50),
|
||||
access_device_ip INET,
|
||||
access_device_location JSONB,
|
||||
auth_device_ip INET,
|
||||
auth_device_name VARCHAR(255),
|
||||
event_type VARCHAR(50),
|
||||
synced_at TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_auth_logs_account ON duo_auth_logs(duo_account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_auth_logs_timestamp ON duo_auth_logs(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_auth_logs_result ON duo_auth_logs(result);
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_auth_logs_user ON duo_auth_logs(user_name);
|
||||
|
||||
-- ── duo_groups ───────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS duo_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
group_id VARCHAR(30) NOT NULL UNIQUE,
|
||||
duo_account_id VARCHAR(20) NOT NULL REFERENCES duo_accounts(account_id) ON DELETE CASCADE,
|
||||
name VARCHAR(255),
|
||||
description TEXT,
|
||||
member_count INTEGER DEFAULT 0,
|
||||
status VARCHAR(50),
|
||||
synced_at TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_groups_account ON duo_groups(duo_account_id);
|
||||
|
||||
-- ── duo_integrations ─────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS duo_integrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
integration_key VARCHAR(50) NOT NULL UNIQUE,
|
||||
duo_account_id VARCHAR(20) NOT NULL REFERENCES duo_accounts(account_id) ON DELETE CASCADE,
|
||||
name VARCHAR(255),
|
||||
type VARCHAR(100),
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
notes TEXT,
|
||||
synced_at TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_duo_integrations_account ON duo_integrations(duo_account_id);
|
||||
Loading…
Add table
Add a link
Reference in a new issue