Skip to content

Add API-key auth and rest/user to the legacy adapter - #17

Merged
ChrisonSimtian merged 2 commits into
mainfrom
feat/legacy-api-key-auth-and-clients
Aug 16, 2026
Merged

Add API-key auth and rest/user to the legacy adapter#17
ChrisonSimtian merged 2 commits into
mainfrom
feat/legacy-api-key-auth-and-clients

Conversation

@ChrisonSimtian

Copy link
Copy Markdown
Collaborator

Why

The legacy write adapter only knew session auth (UNIFI_USERNAME / UNIFI_PASSWORD). Those aren't part of the Homelab's generated secrets.env, so every caller configured from it — including converge-unifi, the path that owns the public :443 port-forward — couldn't authenticate at all:

$ dotnet run --project Infrastructure/engine -- converge-unifi Infrastructure/unifi/network.yaml
Missing UniFi legacy config. Set UNIFI_LEGACY_BASE_URL (…), UNIFI_USERNAME, UNIFI_PASSWORD …

A UniFi OS gateway accepts the same X-API-KEY the integration API uses, so this adds it as a second mode rather than replacing session auth — the .containers/unifi test container can't mint API keys, so session auth is the only thing that works there.

What

  • UnifiLegacyOptions — optional ApiKey; Username/Password become optional; Validate() rejects a half-configured pair on construction instead of as an opaque 401 later; TryFromEnvironment() prefers UNIFI_API_KEY and can derive the site URL from UNIFI_LOCAL_HOST.
  • UnifiLegacySession — API-key mode attaches X-API-KEY and skips login. A 401 is not retried in that mode: there's no session to refresh, so the retry would just repeat the rejection.
  • UnifiLegacyClientrest/user (list, partial update, create): the object a DHCP reservation actually lives on. No delete, deliberately — removing a known client discards its name and history along with the reservation, while UpdateUserAsync with UseFixedIp=false retires one reversibly.

Two things found by testing against real hardware, not the container

UnifiNetwork.Vlan was typed string, but a real gateway sends a number. Only the test container sends a string, so ListNetworksAsync threw on every real controller while the whole container suite stayed green:

UnifiLegacyException : networkconf: unparseable response (200): {"meta":{"rc":"ok"},"data":[…

Reads now accept either form via a small converter; write behaviour is unchanged (creates still send a string).

The destructive live tests are now gated on session auth specifically. They create and delete a port-forward, a firewall group and a VLAN. Once TryFromEnvironment started succeeding on an API key alone, a plain secrets.env would have aimed them at the live home gateway — so LegacyContainerFixture now requires UsesApiKey: false, and read-only live checks got their own fixture.

Verification

30 tests, 27 passing / 3 skipped. The two new read-only live tests ran against the real gateway in API-key mode (list networks, list known clients + reservations); the three destructive ones correctly skipped.

🤖 Generated with Claude Code

The legacy write adapter only knew session auth, so it needed
UNIFI_USERNAME/UNIFI_PASSWORD. Those are not part of the homelab's generated
secrets.env, which means every caller configured from it — including
`converge-unifi`, the path that owns the public :443 port-forward — could not
authenticate at all. A UniFi OS gateway accepts the same X-API-KEY the
integration API uses, so this adds it as a second mode.

- UnifiLegacyOptions: optional ApiKey; Username/Password become optional;
  Validate() rejects a half-configured pair up front; TryFromEnvironment()
  prefers UNIFI_API_KEY and can derive the site URL from UNIFI_LOCAL_HOST.
  Session auth stays — it is the only mode .containers/unifi supports.
- UnifiLegacySession: API-key mode attaches X-API-KEY and skips login entirely.
  A 401 is no longer retried in that mode; there is no session to refresh, so a
  retry would just repeat the same rejection.
- UnifiLegacyClient: rest/user (list, partial update, create) — the object a
  DHCP reservation actually lives on. Deliberately no delete: dropping a known
  client discards its name and history along with the reservation, whereas
  UpdateUserAsync with UseFixedIp=false retires one reversibly.

Two things found while proving it against real hardware rather than the
container:

UnifiNetwork.Vlan was typed string, but a real gateway sends a number and only
the test container sends a string — so ListNetworksAsync threw on every real
controller while the container suite stayed green. Reads now accept either form;
write behaviour is unchanged.

The destructive live tests are now gated on session auth specifically. They
create and delete a port-forward, a firewall group and a VLAN, and once
TryFromEnvironment started succeeding on an API key alone, a plain secrets.env
would have aimed them at the live home gateway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChrisonSimtian ChrisonSimtian added the enhancement New feature or request label Aug 16, 2026
The adapter could create and delete a port-forward but not correct one, so a
caller that detected drift had no way to fix it short of delete-and-recreate —
which drops the rule, and with it any traffic through it, for the gap between
the two calls. Reuses the same partial-update path as rest/user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChrisonSimtian
ChrisonSimtian merged commit af05455 into main Aug 16, 2026
1 check passed
@ChrisonSimtian
ChrisonSimtian deleted the feat/legacy-api-key-auth-and-clients branch August 16, 2026 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant