Add API-key auth and rest/user to the legacy adapter - #17
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The legacy write adapter only knew session auth (
UNIFI_USERNAME/UNIFI_PASSWORD). Those aren't part of the Homelab's generatedsecrets.env, so every caller configured from it — includingconverge-unifi, the path that owns the public:443port-forward — couldn't authenticate at all:A UniFi OS gateway accepts the same
X-API-KEYthe integration API uses, so this adds it as a second mode rather than replacing session auth — the.containers/unifitest container can't mint API keys, so session auth is the only thing that works there.What
UnifiLegacyOptions— optionalApiKey;Username/Passwordbecome optional;Validate()rejects a half-configured pair on construction instead of as an opaque 401 later;TryFromEnvironment()prefersUNIFI_API_KEYand can derive the site URL fromUNIFI_LOCAL_HOST.UnifiLegacySession— API-key mode attachesX-API-KEYand skips login. A 401 is not retried in that mode: there's no session to refresh, so the retry would just repeat the rejection.UnifiLegacyClient—rest/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, whileUpdateUserAsyncwithUseFixedIp=falseretires one reversibly.Two things found by testing against real hardware, not the container
UnifiNetwork.Vlanwas typedstring, but a real gateway sends a number. Only the test container sends a string, soListNetworksAsyncthrew on every real controller while the whole container suite stayed green: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
TryFromEnvironmentstarted succeeding on an API key alone, a plainsecrets.envwould have aimed them at the live home gateway — soLegacyContainerFixturenow requiresUsesApiKey: 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