Files
claudetools/temp/SPEC-005-integration-catalog.md
Mike Swanson afb3a9ec6d sync: auto-sync from GURU-5070 at 2026-05-29 13:48:45
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-05-29 13:48:45
2026-05-29 13:48:51 -07:00

197 lines
12 KiB
Markdown

# SPEC-005: Integration Catalog ("Integration Center")
**Status:** Approved for build (planning locked 2026-05-29)
**Priority:** P2 — built in tandem with SPEC-002 (Syncro PSA)
**Requested By:** Mike Swanson (2026-05-18; scope locked 2026-05-29)
**Estimated Effort:** Large (catalog + Syncro plugin + MSP360 migration)
---
## Overview
The GuruRMM **Integration Center** is a centralized, **partner-scoped** surface for managing third-party integrations (Syncro PSA, MSP360 Managed Backup, and future providers). Each **partner (MSP)** browses the available catalog, configures the integrations they want for their own tenant, and monitors health/status — without per-tool fragmentation.
This is an **internal catalog**, not a public marketplace. App-store-style discovery, monetization/entitlement, and third-party-submitted integrations are explicitly **out of scope** (may be revisited as a separate "Store" initiative later).
**Audience:** Partner-level users (ADR-001 Dev -> Partner -> Client model). Partner-admins configure; partner-users have read-only status visibility. Dev/ACG (partner #1) sees all partners.
**User Benefits**
- Centralized, one-screen management of all integrations for a partner.
- One-click configure with validation; clear status (Not Configured / Configured / Active / Error).
- Per-partner health monitoring + audit trail.
---
## Locked Decisions (2026-05-29)
1. **Partner-scoped.** Every configuration and audit row carries `partner_id NOT NULL REFERENCES partners(id)`. The API derives `partner_id` from the caller's JWT; it is never client-supplied. This mirrors the existing `mspbackups_config(partner_id, UNIQUE(partner_id))` pattern and enforces tenant isolation server-side (closing the same horizontal-privilege class as the known `credentials/:id/reveal` finding).
2. **Generic JSONB config storage.** A single `integration_configurations` table holds all per-partner configs in a `settings JSONB` column. MSP360's existing `mspbackups_config` is **migrated into this table** (see Migration). No per-plugin config tables.
3. **Catalog + Syncro built together.** MSP360 is wrapped as the first plugin (its config migrated in); Syncro (SPEC-002) is implemented as the first *new* plugin through the catalog pattern, proving the full configure flow. Both ship with the catalog.
4. **Available-integration metadata lives in code**, not the DB. The plugin registry (the `IntegrationPlugin` trait implementations) is the source of truth for the catalog listing (name, provider, category, required fields). The DB stores only per-partner configurations, status, and audit — no `integrations` catalog table to drift out of sync with shipped code.
5. **Reuse AES-256-GCM** (migration `016` credentials encryption) for secret-typed fields. Secret fields within `settings` are encrypted at rest and masked on read; non-secret fields stored plaintext in the JSONB.
---
## Scope
### Included (v1)
- Integration Center UI: browse the catalog (from the code registry), configure, and monitor — partner-scoped.
- Plugins: **MSP360** (migrated from standalone) and **Syncro PSA** (new, built alongside).
- One-click configuration flow with per-plugin field validation.
- Status tracking: Not Configured / Configured / Active / Error, with last-health-check + error message.
- Per-partner audit logging of all configuration changes.
- Plugin/extension architecture (trait + registry) so new integrations are additive.
### Out of Scope
- Public/marketplace discovery, third-party-submitted integrations.
- Monetization / licensing / entitlement.
- Multiple instances of the same integration per partner (v1 = one instance per integration per partner; multi-instance deferred to Phase 2).
- Non-IT integrations.
### Success Criteria
- A partner can configure Syncro and MSP360 from one screen; status reflects real health.
- MSP360 migration is transparent — the existing MSPBackups feature keeps working, no partner reconfiguration.
- Tenant isolation verified: no partner can read or affect another partner's integration config.
---
## Architecture
### Plugin registry (code = source of truth for the catalog)
```rust
// server/src/integrations/plugin_interface.rs
#[async_trait]
pub trait IntegrationPlugin: Send + Sync {
fn metadata(&self) -> IntegrationMetadata; // static catalog info
async fn validate(&self, settings: &serde_json::Value) -> Result<()>; // pre-save validation
async fn configure(&self, partner_id: Uuid, settings: serde_json::Value) -> Result<()>;
async fn health_check(&self, partner_id: Uuid) -> Result<HealthStatus>;
}
pub struct IntegrationMetadata {
pub key: String, // stable id, e.g. "syncro", "msp360"
pub name: String,
pub provider: String,
pub category: IntegrationCategory, // Psa, Backup, Rmm, Monitoring, ...
pub description: String,
pub fields: Vec<FieldSpec>, // drives the dynamic config form
}
pub struct FieldSpec {
pub key: String,
pub label: String,
pub kind: FieldKind, // Text | Secret | Select(Vec<String>) | Url | Bool
pub required: bool,
pub help: Option<String>,
}
pub enum HealthStatus { Ok, Degraded(String), Error(String) }
```
A static `IntegrationRegistry` holds `HashMap<String /*key*/, Arc<dyn IntegrationPlugin>>`, built at startup. `GET /api/integrations` returns registry metadata joined with the calling partner's stored config status.
### Data model (generic, partner-scoped)
```sql
-- New: per-partner integration configuration (generic JSONB)
CREATE TABLE integration_configurations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
partner_id UUID NOT NULL REFERENCES partners(id) ON DELETE CASCADE,
integration_key VARCHAR(64) NOT NULL, -- matches plugin registry key
settings JSONB NOT NULL DEFAULT '{}', -- secret-typed fields encrypted at rest
status VARCHAR(20) NOT NULL DEFAULT 'Configured'
CHECK (status IN ('Not Configured','Configured','Active','Error')),
last_health_check TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uk_integration_partner_key UNIQUE (partner_id, integration_key)
);
CREATE INDEX idx_integration_cfg_partner ON integration_configurations(partner_id);
-- New: per-partner audit
CREATE TABLE integration_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
partner_id UUID NOT NULL REFERENCES partners(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id),
integration_key VARCHAR(64) NOT NULL,
action VARCHAR(100) NOT NULL, -- configured | reconfigured | enabled | disabled | health_error
details JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_integration_audit_partner ON integration_audit_logs(partner_id);
CREATE INDEX idx_integration_audit_created ON integration_audit_logs(created_at DESC);
```
> Note: there is intentionally **no `integrations` catalog table** — available integrations come from the code registry (Decision #4).
### MSP360 migration (live integration — handle with care)
`mspbackups_config` is live and partner-scoped already. Migration:
1. Add the new tables (above).
2. Data migration: for each `mspbackups_config` row, insert an `integration_configurations` row with `integration_key='msp360'`, `status='Active'`, and `settings` = the MSP360 fields (secret fields re-encrypted via the credentials AES-256-GCM module).
3. Refactor the `mspbackups` module to read/write its config through the generic store (an accessor that serializes/deserializes the MSP360 settings shape), OR keep `mspbackups_config` as the system of record and have the `msp360` plugin **adapt** to it during a transition window. **Recommended: dual-write + read-from-new behind a flag, verify parity, then drop `mspbackups_config`** in a follow-up migration — never a hard cutover on a working integration.
4. The existing `MSPBackups.tsx` page stays; the Integration Center adds a unified entry that deep-links to it.
> This is the one genuinely risky step. It must ship behind a feature flag with a verified rollback (keep `mspbackups_config` until parity is confirmed in production).
### API (partner-scoped; keyed by plugin key)
- `GET /api/integrations` — catalog (registry metadata) + this partner's status per integration
- `GET /api/integrations/:key` — detail + field spec + current (masked) config + status
- `POST /api/integrations/:key/configure` — validate + store (partner from JWT); audit
- `POST /api/integrations/:key/reconfigure` — update existing
- `POST /api/integrations/:key/test` — run `health_check` on demand
- `DELETE /api/integrations/:key` — remove this partner's configuration
- `GET /api/integrations/:key/audit` — partner-scoped audit log
RBAC: configure/reconfigure/delete require partner-admin; reads require partner membership. `partner_id` always from JWT, never the body/path.
### Health checks
A scheduled job (every ~15 min) iterates each partner's configured integrations, calls `health_check(partner_id)`, updates `status`/`last_health_check`/`error_message`, and writes an audit row on any status transition. Error transitions may raise an alert via the existing alerting subsystem.
### Dashboard
- `pages/IntegrationCenter.tsx` — grid of `IntegrationCard` tiles (name, category, status badge), filterable by category.
- `pages/IntegrationDetail.tsx` — dynamic `ConfigurationForm` rendered from the plugin's `FieldSpec[]`, status panel, audit list, "Test connection" button.
- Reuse the existing status-badge helper pattern (explicit `getStatusBadgeClass()` function — not a `Record` const; see project anti-patterns).
---
## Security
- **Tenant isolation:** every query filters by `partner_id` from JWT. Server-side enforced; covered by tests that attempt cross-partner access.
- **Secrets:** secret-typed fields encrypted at rest (AES-256-GCM, migration 016 module); masked in all API responses; never logged. Audit `details` must redact secrets.
- **Input validation:** each plugin validates `settings` against its `FieldSpec` before persist.
- **Audit:** all config mutations logged with user + partner + action.
---
## Rollout (combined catalog + Syncro)
1. **Infra:** migrations (new tables), plugin trait + registry, health-check scheduler, encryption helpers.
2. **Plugins:** MSP360 adapter (dual-write/verify) + Syncro plugin (SPEC-002 functionality behind the plugin interface).
3. **API:** partner-scoped endpoints + RBAC + tests (incl. cross-partner isolation).
4. **Dashboard:** Integration Center + detail/config form + status/audit.
5. **Cutover:** behind `feature/integration-center` flag; verify MSP360 parity in prod; drop `mspbackups_config` in a follow-up migration.
Holistic-development rule applies: backend + API + dashboard + docs ship together (DESIGN.md).
---
## Dependencies
- **SPEC-002 (Syncro PSA)** — built in tandem; its connection/config logic implemented as the `syncro` plugin.
- **SPEC-004 (MSP360)** — existing; migrated into the catalog as the `msp360` plugin.
- **partners / multi-tenancy (ADR-001)** — already in place (`partners` table, `clients.partner_id`).
## Resolved Open Questions
- Multiple instances per partner? **No in v1** (UNIQUE(partner_id, integration_key)); Phase 2.
- Config storage? **Generic JSONB** (Decision #2).
- Build order? **Catalog + Syncro together** (Decision #3).
- Non-admin visibility? **Read-only status**, yes.
---
## References
- [SPEC-002 Syncro PSA](./SPEC-002-syncro-psa-integration.md), [SPEC-004 MSP360](./SPEC-004-mspbackups-integration.md)
- ADR-001 multi-tenancy (Dev/Partner/Client); migration `016` (credentials AES-256-GCM); `034/035/044` (MSP360)
- Patterns: plugin/registry, generic JSONB config, partner-scoped tenancy