Skip to content

INT-1912: UniFi Network Polarity Integration — V1 (IP & MAC Lookup, Multi-Site, Block/Reconnect) - #1

Open
polarity-developer-agent[bot] wants to merge 3 commits into
masterfrom
feature/INT-1912-unifi-integration
Open

INT-1912: UniFi Network Polarity Integration — V1 (IP & MAC Lookup, Multi-Site, Block/Reconnect)#1
polarity-developer-agent[bot] wants to merge 3 commits into
masterfrom
feature/INT-1912-unifi-integration

Conversation

@polarity-developer-agent

Copy link
Copy Markdown

Summary

Implements the full V1 UniFi Network Polarity integration as specified in INT-1912.

Security analysts can hover over any IPv4 or MAC Address in Polarity to see real-time context from their UniFi Network controller, and block or reconnect suspicious clients directly from the overlay.


Files Created (13 total)

File Description
integration.js Main integration: startup, doLookup, onMessage, validateOptions. Multi-site fan-out via async.parallelLimit. Module-level sites cache (1 hr TTL).
config/config.js Options: url (adminOnly text), apiKey (adminOnly password), blocklist, ipBlocklistRegex
config/config.json Standard empty placeholder {}
components/block.js Ember component with per-client isBlockingClient loading state and blockActionMessage inline feedback
components/summary.js Minimal summary component aliasing block.data.details
templates/block.hbs Detail view: CollapsibleSection per client/device with identity, network, traffic tables and block/reconnect buttons
templates/summary.hbs Summary pills: type badge, status badge, hostname/device name, site name
reducers/index.js Standard empty reducers module.exports = {}
package.json Dependencies: postman-request, async, lodash, polarity-integration-utils
.github/workflows/release-current-version.yml Standard release workflow (from greynoise reference)
.github/workflows/run-int-dev-checklist.yml Standard PR checklist workflow (from greynoise reference)
.gitignore Standard integration gitignore (from greynoise reference)
README.md Controller URL format, API key generation, entity types, block/reconnect, multi-site behavior

API Coverage

Endpoint Status
GET /v1/sites ✅ 1-hour module-level cache (sitesCache + sitesCacheExpiry)
GET /v1/sites/{siteId}/clients?filter=ipAddress.eq(...) ✅ IP lookup
GET /v1/sites/{siteId}/clients?filter=macAddress.eq(...) ✅ MAC lookup
GET /v1/sites/{siteId}/devices?filter=... ✅ With transparent 400 fallback to full list + client-side filter
POST /v1/sites/{siteId}/clients/{clientId}/actions block and reconnect

Key Design Decisions

  • Multi-site fan-out: N_sites × 2 parallel tasks (clients + devices per site) via async.parallelLimit(10)
  • clientId UUID: Stored on every details.clients[] element so onMessage can access it for block/reconnect
  • Device filter fallback: 400 response → silently falls back to GET /devices full list + client-side filter
  • No trailing slash: validateOptions rejects URLs ending in /
  • Error handling: 200/404/401/403/400/network dispatch matching efficient-ip pattern exactly

Reference Patterns Used

Pattern Source
postman-request defaults, requestWithDefaults polarityio/efficient-ip
IGNORED_IPS, _isInvalidEntity, _isEntityBlocklisted, _setupRegexBlocklists polarityio/efficient-ip
async.parallelLimit fan-out, callback-style task polarityio/efficient-ip
handleRestError status dispatch polarityio/efficient-ip
onMessage + switch (payload.action) polarityio/redmine
CI workflows polarityio/greynoise

Testing Checklist

  • npm install completes without errors
  • Controller URL with trailing slash is rejected in validateOptions
  • Empty API Key is rejected in validateOptions
  • Sites cache populates on first doLookup; cache hit on second call
  • IPv4 lookup returns correct client results with siteId + siteName attached
  • MAC Address lookup returns correct device results
  • Multi-site: results from multiple sites appear in separate collapsible sections
  • Block action: loading spinner shown; client status changes to BLOCKED in overlay
  • Reconnect action: client status changes to CONNECTED in overlay
  • Infrastructure device results show no action buttons
  • 401 → "Invalid API Key" user-facing error
  • 403 → "Forbidden" user-facing error
  • Network error → "HTTP Request Error" user-facing error

Closes INT-1912

Polarity Dev Agent and others added 3 commits May 21, 2026 18:51
- Multi-site client and device lookup (IPv4, MAC Address)
- GET /v1/sites with 1-hour module-level cache
- Client search: GET /v1/sites/{siteId}/clients?filter=ipAddress.eq() or macAddress.eq()
- Device search: GET /v1/sites/{siteId}/devices?filter=... with 400 fallback to full list + client-side filter
- onMessage: BLOCK_CLIENT and RECONNECT_CLIENT actions via POST /v1/sites/{siteId}/clients/{clientId}/actions
- Ember block.js component: per-client isBlockingClient loading state and blockActionMessage feedback
- block.hbs: CollapsibleSection per client/device with p-key/p-value identity, network, traffic tables
- summary.hbs: type badge, status badge, hostname/device name, site name pills
- validateOptions: URL trailing slash enforcement, API key required check
- handleRestError: 200/404/401/403/400/network error dispatch (efficient-ip pattern)
- package.json: postman-request, async, lodash, polarity-integration-utils
- CI: release-current-version + run-int-dev-checklist workflows (greynoise reference)
config/config.js
- Add missing top-level fields: name, acronym, description, entityTypes,
  defaultColor, styles, onDemandOnly, block, and request sections
- Add request section (cert/key/proxy/rejectUnauthorized) — absence caused
  startup() to throw 'cannot destructure undefined' on every server start
- Set rejectUnauthorized: false (UniFi controllers use self-signed certs)

config/config.json
- Was shipped as {} — silent install failure on the Polarity server
- Fully populate as exact mirror of config.js (all 4 options, request,
  logging, block, styles, entityTypes, defaultColor, onDemandOnly)
- Add real UUID v4: dd4d5ee5-2097-4584-9af4-2e352aa74ffe
- Add reducer.details.file pointer to ./reducers/details.json

reducers/details.json (new file)
- Create pipeline DSL with copy ops for client_names, client_ips,
  client_macs, client_statuses, client_sites, device_names, device_models,
  device_states, device_sites + prune empty_values pass

styles/styles.less (new file)
- Create scoped LESS required by config.js styles[] reference
- Collapsible section toggle, item entries, status colours (connected/
  blocked/updating), action button area, working spinner, error banner

components/block.js
- Add init() with this._super(...arguments) — required by PolarityComponent
- Move isBlocking and actionMessage maps inside init() under block._state
  so they are per-instance, not class-level shared objects
- Add hasClients and hasDevices computed properties to guard {{#each}}
- Route all action state through block._state.isBlocking / actionMessage
  for proper Ember reactivity

templates/block.hbs
- Remove {{#block-section}} — not a real Polarity component; rendered blank
- Remove {{#each}} loops without has* computed property guards
- Replace (not-eq ...) with (not (eq ...)) — 'not-eq' is not a registered
  Ember helper; causes silent blank panel at runtime
- Remove format-uptime and format-bytes helpers — not registered in
  Polarity's Ember build; show raw uptime seconds and remove byte display
- Remove @index inside {{#each}} — not supported in Polarity's Ember build
- Rewrite using standard p-key/p-value rows, fa-icon, collapsible sections
  via block._state.showClients / showDevices + toggleSection action
- Route action loading/message state through block._state.isBlocking and
  block._state.actionMessage

integration.js
- _searchClients: explicitly normalise clientId from c.id || c.clientId ||
  c._id — UniFi API returns UUID as 'id'; missing mapping broke all
  block/reconnect actions silently
- _searchDevices: remove devices.length > 1 heuristic filter guard;
  always apply client-side filter to handle controllers that ignore the
  filter query param regardless of result count
- _isEntityBlocklisted: parse comma-separated blocklist string into an
  array before comparison; previous _.includes() on raw string did a
  substring check producing false-positive blocklist hits
- _setupRegexBlocklists: add null-guard (options.ipBlocklistRegex || '')
  before accessing .length to prevent TypeError on undefined option
The UniFi Network Integration API v1 returns the site identifier in the
'id' field (UUID), not 'siteId'. Reading site.siteId produced undefined,
which was interpolated as the literal string 'undefined' into the URL:

  GET /v1/sites/undefined/clients?filter=...

This triggered the API error: 'undefined' is not a valid 'siteId' value.

Fix: read site.id in doLookup when extracting the siteId for _searchClients
and _searchDevices. All downstream uses (enriched client/device objects,
onMessage BLOCK_CLIENT/RECONNECT_CLIENT payloads via client.siteId) are
correct — they all read the enriched property set from this same extraction
point.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant