* feat(external-auth)!: require a permission to author policy default roles
Setting the defaultRoleIds of an unlinked-identity policy was guarded only by
the subset rule -- you could not grant roles carrying permissions you did not
hold -- so any actor able to edit a connection could decide what auto-created
users receive. The permission named for that decision,
external-authentication/policies/default-roles:update, was enforced in one
place: removing policy references while deleting a role.
The asymmetry is what makes this look like a check that was never wired
rather than a deliberate carve-out. Its sibling, policies:update, is already
enforced on the write path at both the create and update sites, through the
same RequiresPolicyManagement condition that covers the very policy the roles
live inside.
Demonstrated rather than argued: with the guard stubbed out, a caller holding
only connections:create and policies:update creates a connection whose policy
assigns "workflow-user", and the response is 201. The subset rule does not
object, because it answers a different question -- it prevents escalation, not
delegation of the decision.
The two checks are now reported independently for that reason. The permission
asks whether this actor may decide default roles at all; the subset rule asks
whether these particular roles stay inside what they already hold. It applies
only when roles are actually being set, so clearing the list, or a policy that
assigns none, needs nothing extra.
Breaking for roles holding the legacy policies:manage but not roles:assign
that set default roles today. Anyone who held roles:assign already maps to the
new permission and is unaffected. Documented in the migration guide.
Closes#7977
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(external-auth): gate default roles on the set changing, not on it existing
Review reproduced the over-reach through the real endpoints: validation runs
on every update, on enabling a connection, and on read-only validate, so
keying the permission off default roles being present meant that once anyone
set them, an administrator without the permission could no longer edit an
unrelated field on that connection, enable it, or validate it.
The permission now applies when the set changes -- adding, removing, or
clearing all count as deciding what auto-created users receive; leaving a
stored set alone does not. Order is not treated as meaningful, so reordering
is not a change.
The test that was supposed to cover this asserted only that a message was
absent, which passes for any failure response and made it vacuous exactly
when it mattered: it passed with the over-reach still in place, because the
request was failing 405 on the wrong verb. It now uses PUT and asserts
success, and reverting the fix makes it fail with the 400 review described.
Refs #7977
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(external-auth): treat abandoning a create-user policy as a role change
The permission check sat inside the create-user branch, so it only ran when
the candidate policy still created users. Switching a stored fallback to one
that does not -- 'reject', or match-user with a different noMatchAction --
skipped it entirely and dropped the policy's automatic role assignments
without the permission that governs them. Review reproduced it.
The effective default roles of a policy that does not create users are none,
so computing that first and comparing outside the branch makes abandonment a
change like any other. The subset rule stays inside the branch, because it
only has something to say about roles actually being assigned.
The new test expresses abandonment through noMatchAction rather than the
policy type, since the fixture's registry only knows match-user. Re-scoping
the check to create-user candidates makes it fail with OK instead of the
expected BadRequest, which is the bypass.
Refs #7977
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(external-auth): take the default-role baseline from the registry
A configuration-owned connection has no database row, so comparing against
the store alone made its configured default roles look newly assigned on
every validation. Validation needs only connections:view, so a caller with
exactly that could not validate such a connection at all -- review
reproduced it.
The baseline now comes from the registry, which answers for both ownerships
and is the question actually being asked: what does this connection assign
today. The store remains a fallback for a record the registry does not know.
The new test gives the fixture's configuration connection an unlinked policy
with default roles and validates it as a view-only caller. Reverting to the
store-only baseline makes it fail with the permission error, which is the
symptom review described.
Refs #7977
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>