<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:blog="https://jonesrussell.github.io/blog/ns"><channel><title>Authorization on Web Developer Blog</title><link>https://jonesrussell.github.io/blog/tags/authorization/</link><description>Recent content in Authorization on Web Developer Blog</description><image><title>Web Developer Blog</title><url>https://jonesrussell.github.io/blog/images/og-default.png</url><link>https://jonesrussell.github.io/blog/images/og-default.png</link></image><generator>Hugo -- 0.169.0</generator><language>en-us</language><lastBuildDate>Sat, 31 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jonesrussell.github.io/blog/tags/authorization/feed.xml" rel="self" type="application/rss+xml"/><item><title>Delegate account management without granting admin</title><link>https://jonesrussell.github.io/blog/delegate-account-management-without-granting-admin/</link><pubDate>Sat, 31 Oct 2026 00:00:00 +0000</pubDate><guid>https://jonesrussell.github.io/blog/delegate-account-management-without-granting-admin/</guid><category>php</category><blog:tag>php</blog:tag><blog:tag>authorization</blog:tag><blog:tag>security</blog:tag><blog:tag>testing</blog:tag><description>Add narrowly scoped account-management authority without handing a service role every power carried by an administrator permission.</description><content:encoded><![CDATA[<p>Ahnii!</p>
<p>A staff member may need to update accounts in one approved roster without gaining every power carried by an administrator role. That is a delegation problem, not a reason to widen the administrator boundary.</p>
<p>This post covers a versioned Waaseyaa authorization change that added consumer-scoped account management, then narrowed it through review. The useful pattern is to delegate exact decisions while keeping privileged fields, account creation, deletion, and credentials behind their existing boundaries.</p>
<h2 id="start-with-the-authority-you-actually-need">Start with the authority you actually need</h2>
<p>A broad permission such as <code>administer users</code> can authorize much more than the workflow requires. It may cover role changes, account deletion, and targets outside the consumer&rsquo;s roster.</p>
<p>Write down the smaller decision instead:</p>
<ul>
<li>Which authenticated actor may act?</li>
<li>Which exact account may they update?</li>
<li>Which operation may they perform?</li>
<li>Which fields must remain unavailable?</li>
<li>What evidence binds the target to the consumer&rsquo;s scope?</li>
</ul>
<p>The framework extension introduced an optional delegate inside the canonical user access policy. The consumer supplies the roster and consent decision. The framework continues to compose the final authorization result.</p>
<p>Without a delegate binding, the old refusal remains. This makes delegation an explicit application choice instead of a new default permission.</p>
<h2 id="pass-immutable-authorization-input">Pass immutable authorization input</h2>
<p>The delegate receives an authenticated authorization principal and the exact target account. It does not receive a mutable account object that application code can alter before asking for a decision.</p>
<p>That distinction matters in retained workers and long-running processes. Authorization should depend on a snapshot of identity, roles, and permissions for the current request. It should not retain an actor from an earlier request.</p>
<p>The provider-discovery tests exercised both bound and unbound policies. They also sent different actors through the same retained handler and then returned to the first actor. That proves the delegate binding survives discovery without turning request identity into service state.</p>
<p>Direct legacy callers still pass through a runtime type guard. An authenticated object is not enough. Delegated decisions require the immutable principal contract.</p>
<h2 id="keep-creation-behind-a-separate-intake-boundary">Keep creation behind a separate intake boundary</h2>
<p>The first version of the delegate included an actor-only account creation decision. Review removed it.</p>
<p>An actor-only predicate can prove that a clerk is generally trusted, but it cannot prove that this new account belongs to an approved roster entry or verified consent record. There is no target yet, so the most important subject of the decision is missing.</p>
<p>The hardened design keeps generic account creation administrator-only. Consumer creation needs a separate provisioning boundary with an explicit approved-intake reference.</p>
<p>This is a useful review question for any delegated API: <strong>does the method receive enough evidence to authorize the thing it creates?</strong> If it receives only the actor, it may be too broad for the workflow it claims to protect.</p>
<h2 id="do-not-release-privileged-fields">Do not release privileged fields</h2>
<p>Permission to update an account does not imply permission to edit every account field.</p>
<p>The delegate cannot authorize edits to:</p>
<ul>
<li>Roles or permissions</li>
<li>Email verification state</li>
<li>Password or legacy password material</li>
<li>Two-factor secrets and recovery data</li>
<li>Session-generation data</li>
<li>Generic active-status fields</li>
<li>Account deletion</li>
</ul>
<p>Those restrictions stay in the canonical field and entity policies. A consumer cannot bypass them by returning <code>true</code> from its delegate.</p>
<p>The initial candidate allowed delegated active-status edits through the generic field surface. The integrated behavior moved that change to a dedicated lifecycle operation. This is the safer boundary because disabling an account has a required side effect: existing sessions must be revoked.</p>
<h2 id="couple-status-changes-to-session-revocation">Couple status changes to session revocation</h2>
<p>The account lifecycle service checks two independent decisions before changing status:</p>
<ol>
<li>The actor may update this exact target.</li>
<li>The actor may perform the dedicated active-status operation.</li>
</ol>
<p>It also refuses self-administration through this path.</p>
<p>When disabling an account, the service increments the session generation and changes the active flag before one repository save. Existing sessions bound to the previous generation become invalid with the same durable write that disables the account.</p>
<p>Re-enabling the account does not restore the old generation. An unchanged status produces no write, and an exhausted generation counter fails instead of wrapping.</p>
<p>That is why the generic field write was not enough. A security-sensitive state change belongs behind the operation that can enforce all of its invariants.</p>
<h2 id="test-the-refusals-as-part-of-the-contract">Test the refusals as part of the contract</h2>
<p>The useful tests are not limited to the admitted clerk and target. They also prove that the boundary refuses:</p>
<ul>
<li>Another actor</li>
<li>Another target</li>
<li>Anonymous input</li>
<li>A mutable legacy account object</li>
<li>Account creation without approved intake</li>
<li>Privileged and credential fields</li>
<li>Deletion</li>
<li>Status changes without both delegate decisions</li>
<li>Self-disable through the lifecycle service</li>
</ul>
<p>Keep those negative cases beside the success path. They describe the authority the delegate does not have, which is the most important part of a narrow delegation contract.</p>
<p>Delegation works when it adds one bounded capability without creating a second administrator role. Pass immutable request identity, bind decisions to exact targets, keep creation tied to verified intake, and route security-sensitive mutations through services that enforce their required side effects.</p>
<p>Baamaapii</p>
]]></content:encoded></item></channel></rss>