Skip to content

fix: act on the Copilot review (role scope widening, host id normalisation) - #96

Merged
malickyeu merged 1 commit into
mainfrom
fix/copilot-review-cleanup
Jul 30, 2026
Merged

fix: act on the Copilot review (role scope widening, host id normalisation)#96
malickyeu merged 1 commit into
mainfrom
fix/copilot-review-cleanup

Conversation

@malickyeu

Copy link
Copy Markdown
Contributor

Summary

Acts on the Copilot review across today's merged PRs (#90#93). Nine comments;
two were real defects, the rest consistency and clarity.

Type of change

  • Bug fix
  • New feature
  • Docs only
  • Refactor / chore

Checklist

  • go test -short ./... and go vet ./... pass
  • gofmt gate is clean (gofmt -l $(git ls-files '*.go') after staging)
  • Frontend type-checks (cd web && npx tsc --noEmit)
  • Rebuilt and committed web/dist
  • Added/updated tests for the change
  • Updated docs/ and added a CHANGELOG.md entry for user-facing changes

The two that mattered

A role scoped to only-invalid hosts became unscoped instead of being refused.
hostIds: [0] was sanitised down to an empty list — which means every host. So a
request to narrow a role quietly produced an unrestricted one, and the audit
line described a scope that wasn't there. Now a 400, matching the rule this codebase
already applies to MCP token section scopes: an explicit narrowing that filters to
nothing is an error, never a fall-through to "everything".

A non-positive ?host= is the local daemon everywhere else. docker.Manager.Client
has always resolved hostID <= 0 to the local daemon, but the new host-scope check
took it literally: ?host=-1 was served locally while being authorised and audited
as host −1, so a scoped user was refused something they are in fact allowed.
Normalised at all three entry points — REST, the WebSocket subscribe frame, and MCP
tool arguments.

Both are mutation-tested: restoring either behaviour fails its own test.

The rest

  • DeleteRole's LDAP-fallback guard was skipped when GetLDAP errored rather
    than failing closed — the one situation the guard exists for.
  • ExistingRoleIDs loaded each role's sections just to test existence, making an
    LDAP login N+1. Now an existence probe.
  • The role handlers audited the raw hostIds rather than the stored ones.
  • The profile page spun forever when loading permissions failed: "still loading"
    and "failed" were the same state. It now says what went wrong and offers a retry.
  • The Access badge hard-coded 13 sections; it's now omitted for admins and while
    loading, rather than baking in a count that will drift.
  • toggleHost derived from the render's state instead of the previous one.
  • RoleIDs serialised as null for configs written before roles existed —
    omitempty keeps them genuinely absent.

One I disagreed with, and what I did instead

Copilot flagged that the TOTPPending comment promises secrets are "discarded
otherwise" while nothing clears them on cancel. The comment was wrong, not the
code.
A stale pending secret is read by exactly one function, which only ever
promotes it after validating a code from the new device, so it grants nothing;
clearing it eagerly would add a cancel path that can itself fail, on a flow whose
entire point is that failure changes nothing. The comment now says that, and warns
against reading its presence as "a pairing is in progress".

Nine comments across #90-#93. Two were real defects, the rest were consistency
and clarity.

An explicit role host scope that sanitised to nothing became UNSCOPED rather
than being refused: hostIds:[0] cleaned to an empty list, which means every
host, so a request to narrow a role produced an unrestricted one and the audit
line described a scope that wasn't there. Now a 400, matching the rule already
applied to MCP token section scopes.

A non-positive ?host= is the local daemon to the Docker layer, but the new
host-scope check took it literally: ?host=-1 was served locally while being
authorised and audited as host -1, refusing a scoped user something they are
allowed. Normalised at all three entry points — REST, the WS subscribe frame
and MCP tool args.

Also: DeleteRole's LDAP-fallback guard was skipped when GetLDAP errored instead
of failing closed; ExistingRoleIDs loaded each role's sections just to test
existence, making an LDAP login N+1; the role handlers audited the raw hostIds
rather than the stored ones; the profile page spun forever when loading
permissions failed, because "loading" and "failed" were the same state; the
Access badge hard-coded 13 sections; toggleHost derived from the render's state
instead of the previous one; RoleIDs serialised as null for pre-roles configs;
and the TOTPPending comment claimed a discard that never happens (the comment
was wrong, not the code — a stale pending secret is unreadable by anything
except the confirm path, and clearing it eagerly would add a cancel path that
can itself fail).

2 tests, 1 pentest, both verified to fail on the reported behaviour.
Copilot AI review requested due to automatic review settings July 30, 2026 19:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@malickyeu
malickyeu merged commit e648694 into main Jul 30, 2026
3 checks passed
@malickyeu
malickyeu deleted the fix/copilot-review-cleanup branch July 30, 2026 19:27
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.

2 participants