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() orchestration, per-child sequential sync, incremental auth logs - Company matching: exact then 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 Verified data: 33 accounts, 832 users, 925 phones, 5927 auth logs, 46 groups, 78 integrations Also: entity-mapper company fields update, task list marked complete
This commit is contained in:
parent
a4242b81be
commit
e3aba93857
3 changed files with 88 additions and 41 deletions
|
|
@ -127,17 +127,59 @@ export interface Company {
|
|||
alternatePhone1?: string;
|
||||
alternatePhone2?: string;
|
||||
fax?: string;
|
||||
webSiteURL?: string;
|
||||
webAddress?: string;
|
||||
address1?: string;
|
||||
address2?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
postalCode?: string;
|
||||
country?: string;
|
||||
countryID?: number;
|
||||
additionalAddressInformation?: string;
|
||||
companyType?: number;
|
||||
companyCategoryID?: number;
|
||||
classification?: string;
|
||||
territoryID?: number;
|
||||
marketSegmentID?: number;
|
||||
competitorID?: number;
|
||||
parentCompanyID?: number;
|
||||
ownerResourceID?: number;
|
||||
billingAddress1?: string;
|
||||
billingAddress2?: string;
|
||||
billToCity?: string;
|
||||
billToState?: string;
|
||||
billToZipCode?: string;
|
||||
billToCountryID?: number;
|
||||
billToAddressToUse?: number;
|
||||
billToAttention?: string;
|
||||
billToAdditionalAddressInformation?: string;
|
||||
billToCompanyLocationID?: number;
|
||||
taxID?: string;
|
||||
isTaxExempt?: boolean;
|
||||
taxRegionID?: number;
|
||||
currencyID?: number;
|
||||
invoiceMethod?: number;
|
||||
invoiceTemplateID?: number;
|
||||
invoiceEmailMessageID?: number;
|
||||
invoiceNonContractItemsToParentCompany?: boolean;
|
||||
quoteTemplateID?: number;
|
||||
quoteEmailMessageID?: number;
|
||||
purchaseOrderTemplateID?: number;
|
||||
keyAccountIcon?: number;
|
||||
isClientPortalActive?: boolean;
|
||||
isTaskFireActive?: boolean;
|
||||
isEnabledForComanaged?: boolean;
|
||||
sicCode?: string;
|
||||
stockMarket?: string;
|
||||
stockSymbol?: string;
|
||||
surveyCompanyRating?: number;
|
||||
assetValue?: number;
|
||||
apiVendorID?: number;
|
||||
lastActivityDate?: string;
|
||||
lastTrackedModifiedDateTime?: string;
|
||||
createDate?: string;
|
||||
createdByResourceID?: number;
|
||||
impersonatorCreatorResourceID?: number;
|
||||
userDefinedFields?: Array<{ name: string; value: string | null }>;
|
||||
}
|
||||
|
||||
export interface TicketNote {
|
||||
|
|
|
|||
|
|
@ -169,6 +169,11 @@ function mapCompany(data: any): Record<string, any> {
|
|||
api_vendor_id: data.apiVendorID,
|
||||
create_date: data.createDate,
|
||||
created_by_resource_id: data.createdByResourceID,
|
||||
classification: data.classification ?? null,
|
||||
bill_to_company_location_id: data.billToCompanyLocationID ?? null,
|
||||
impersonator_creator_resource_id: data.impersonatorCreatorResourceID ?? null,
|
||||
invoice_non_contract_items_to_parent_company: data.invoiceNonContractItemsToParentCompany ?? null,
|
||||
quote_email_message_id: data.quoteEmailMessageID ?? null,
|
||||
user_defined_fields: Object.keys(udfs).length > 0 ? JSON.stringify(udfs) : null,
|
||||
is_deleted: data.isDeleted || false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,43 +23,43 @@
|
|||
|
||||
## Tasks
|
||||
|
||||
- [ ] 1.0 Create the Duo API Client (`lib/services/duo-client.ts`)
|
||||
- [x] 1.0 Create the Duo API Client (`lib/services/duo-client.ts`)
|
||||
- [x] 1.1 Implement `DuoClient` class with constructor accepting `ikey`, `skey`, `host`
|
||||
- [ ] 1.2 Implement HMAC-SHA1 request signing method (canon string → signature → Basic auth header)
|
||||
- [ ] 1.3 Implement `get(path, params)` and `post(path, params)` methods with signed HTTPS requests
|
||||
- [ ] 1.4 Implement automatic pagination — follow `metadata.next_offset` until all pages retrieved
|
||||
- [ ] 1.5 Implement rate-limit handling — detect HTTP 429, read `Retry-After` header, wait and retry
|
||||
- [ ] 1.6 Add configurable timeout (default 30s) on all requests
|
||||
- [ ] 1.7 Implement Accounts API method: `listAccounts()` → `POST /accounts/v1/account/list`
|
||||
- [ ] 1.8 Implement Admin API methods: `getAccountSummary()`, `getUsers()`, `getPhones()`, `getGroups()`, `getIntegrations()`, `getAuthLogs()`
|
||||
- [ ] 1.9 Support child account access pattern — accept override `host` + `account_id` param for Admin API calls
|
||||
- [ ] 2.0 Create Database Migration (`migrations/058_create_duo_tables.sql`)
|
||||
- [ ] 2.1 Create `duo_accounts` table with all columns from PRD (account_id, name, api_hostname, autotask_company_id FK, user_count, integration_count, edition, is_parent, synced_at, created_at)
|
||||
- [ ] 2.2 Create `duo_users` table (user_id, duo_account_id FK, username, email, realname, status, is_enrolled, last_login, groups JSONB, aliases JSONB, etc.)
|
||||
- [ ] 2.3 Create `duo_phones` table (phone_id, duo_account_id FK, name, number, type, platform, model, os_version, activated, last_seen, capabilities JSONB, users JSONB)
|
||||
- [ ] 2.4 Create `duo_auth_logs` table (txid, duo_account_id FK, timestamp, user_name, factor, result, reason, access_device_ip INET, access_device_location JSONB, etc.)
|
||||
- [ ] 2.5 Create `duo_groups` table (group_id, duo_account_id FK, name, description, member_count, status)
|
||||
- [ ] 2.6 Create `duo_integrations` table (integration_key, duo_account_id FK, name, type, enabled, notes)
|
||||
- [ ] 2.7 Add indexes: duo_account_id on all child tables, timestamp on auth_logs, status on users, is_parent on accounts
|
||||
- [ ] 3.0 Create the Duo Sync Service (`lib/services/duo-sync-service.ts`)
|
||||
- [ ] 3.1 Implement `syncAccounts()` — list child accounts via Accounts API, upsert into `duo_accounts`, add parent account row
|
||||
- [ ] 3.2 Implement `syncAccountData(account)` — for a single account, sync users, phones, groups, integrations via Admin API upserts
|
||||
- [ ] 3.3 Implement `syncAuthLogs(account, since?)` — incremental auth log sync using `mintime` from last synced timestamp
|
||||
- [ ] 3.4 Implement `syncAll()` — orchestrate full sync: syncAccounts → loop each child sequentially → syncAccountData + syncAuthLogs → sync parent account
|
||||
- [ ] 3.5 Implement company matching — after syncing accounts, match `duo_accounts.name` to `companies.company_name` (exact → case-insensitive containment → skip)
|
||||
- [ ] 3.6 Add sync ID tracking, progress logging, and per-account stats (records added/updated)
|
||||
- [ ] 4.0 Create Internal API Routes
|
||||
- [ ] 4.1 Create `app/api/duo/sync/route.ts` — POST to trigger full sync (non-blocking), GET to return sync status
|
||||
- [ ] 4.2 Create `app/api/duo/accounts/route.ts` — GET to list all Duo accounts with user_count, integration_count, matched company
|
||||
- [ ] 4.3 Create `app/api/duo/accounts/[id]/users/route.ts` — GET to list users for a specific Duo account
|
||||
- [ ] 5.0 Create OpenClaw Route and Register in Middleware
|
||||
- [ ] 5.1 Create `app/api/openclaw/sync/duo/route.ts` — POST with API key auth, triggers `syncAll()` non-blocking
|
||||
- [ ] 5.2 Add `/api/duo` to publicRoutes in `middleware.ts`
|
||||
- [ ] 6.0 End-to-End Testing — Run Migration, Build, Sync, Verify
|
||||
- [ ] 6.1 Run migration 058 against pulse-postgres
|
||||
- [ ] 6.2 Rebuild and restart the app container
|
||||
- [ ] 6.3 Trigger full sync via `/api/duo/sync` POST and verify it completes
|
||||
- [ ] 6.4 Verify all 6 tables populated: duo_accounts (33 rows), duo_users, duo_phones, duo_auth_logs, duo_groups, duo_integrations
|
||||
- [ ] 6.5 Verify company matching — check duo_accounts.autotask_company_id is populated for matched accounts
|
||||
- [ ] 6.6 Verify OpenClaw trigger works via `/api/openclaw/sync/duo`
|
||||
- [ ] 6.7 Git commit all changes
|
||||
- [x] 1.2 Implement HMAC-SHA1 request signing method (canon string → signature → Basic auth header)
|
||||
- [x] 1.3 Implement `get(path, params)` and `post(path, params)` methods with signed HTTPS requests
|
||||
- [x] 1.4 Implement automatic pagination — follow `metadata.next_offset` until all pages retrieved
|
||||
- [x] 1.5 Implement rate-limit handling — detect HTTP 429, read `Retry-After` header, wait and retry
|
||||
- [x] 1.6 Add configurable timeout (default 30s) on all requests
|
||||
- [x] 1.7 Implement Accounts API method: `listAccounts()` → `POST /accounts/v1/account/list`
|
||||
- [x] 1.8 Implement Admin API methods: `getAccountSummary()`, `getUsers()`, `getPhones()`, `getGroups()`, `getIntegrations()`, `getAuthLogs()`
|
||||
- [x] 1.9 Support child account access pattern — accept override `host` + `account_id` param for Admin API calls
|
||||
- [x] 2.0 Create Database Migration (`migrations/058_create_duo_tables.sql`)
|
||||
- [x] 2.1 Create `duo_accounts` table with all columns from PRD (account_id, name, api_hostname, autotask_company_id FK, user_count, integration_count, edition, is_parent, synced_at, created_at)
|
||||
- [x] 2.2 Create `duo_users` table (user_id, duo_account_id FK, username, email, realname, status, is_enrolled, last_login, groups JSONB, aliases JSONB, etc.)
|
||||
- [x] 2.3 Create `duo_phones` table (phone_id, duo_account_id FK, name, number, type, platform, model, os_version, activated, last_seen, capabilities JSONB, users JSONB)
|
||||
- [x] 2.4 Create `duo_auth_logs` table (txid, duo_account_id FK, timestamp, user_name, factor, result, reason, access_device_ip INET, access_device_location JSONB, etc.)
|
||||
- [x] 2.5 Create `duo_groups` table (group_id, duo_account_id FK, name, description, member_count, status)
|
||||
- [x] 2.6 Create `duo_integrations` table (integration_key, duo_account_id FK, name, type, enabled, notes)
|
||||
- [x] 2.7 Add indexes: duo_account_id on all child tables, timestamp on auth_logs, status on users, is_parent on accounts
|
||||
- [x] 3.0 Create the Duo Sync Service (`lib/services/duo-sync-service.ts`)
|
||||
- [x] 3.1 Implement `syncAccounts()` — list child accounts via Accounts API, upsert into `duo_accounts`, add parent account row
|
||||
- [x] 3.2 Implement `syncAccountData(account)` — for a single account, sync users, phones, groups, integrations via Admin API upserts
|
||||
- [x] 3.3 Implement `syncAuthLogs(account, since?)` — incremental auth log sync using `mintime` from last synced timestamp
|
||||
- [x] 3.4 Implement `syncAll()` — orchestrate full sync: syncAccounts → loop each child sequentially → syncAccountData + syncAuthLogs → sync parent account
|
||||
- [x] 3.5 Implement company matching — after syncing accounts, match `duo_accounts.name` to `companies.company_name` (exact → case-insensitive containment → skip)
|
||||
- [x] 3.6 Add sync ID tracking, progress logging, and per-account stats (records added/updated)
|
||||
- [x] 4.0 Create Internal API Routes
|
||||
- [x] 4.1 Create `app/api/duo/sync/route.ts` — POST to trigger full sync (non-blocking), GET to return sync status
|
||||
- [x] 4.2 Create `app/api/duo/accounts/route.ts` — GET to list all Duo accounts with user_count, integration_count, matched company
|
||||
- [x] 4.3 Create `app/api/duo/accounts/[id]/users/route.ts` — GET to list users for a specific Duo account
|
||||
- [x] 5.0 Create OpenClaw Route and Register in Middleware
|
||||
- [x] 5.1 Create `app/api/openclaw/sync/duo/route.ts` — POST with API key auth, triggers `syncAll()` non-blocking
|
||||
- [x] 5.2 Add `/api/duo` to publicRoutes in `middleware.ts`
|
||||
- [x] 6.0 End-to-End Testing — Run Migration, Build, Sync, Verify
|
||||
- [x] 6.1 Run migration 058 against pulse-postgres
|
||||
- [x] 6.2 Rebuild and restart the app container
|
||||
- [x] 6.3 Trigger full sync via `/api/duo/sync` POST and verify it completes
|
||||
- [x] 6.4 Verify all 6 tables populated: duo_accounts (33 rows), duo_users, duo_phones, duo_auth_logs, duo_groups, duo_integrations
|
||||
- [x] 6.5 Verify company matching — check duo_accounts.autotask_company_id is populated for matched accounts
|
||||
- [x] 6.6 Verify OpenClaw trigger works via `/api/openclaw/sync/duo`
|
||||
- [x] 6.7 Git commit all changes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue