<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: allowfy</title>
    <description>The latest articles on DEV Community by allowfy (@allowfy_team).</description>
    <link>https://dev.to/allowfy_team</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4205003%2F79df9cd2-3734-4851-80ed-4df70913fe90.png</url>
      <title>DEV Community: allowfy</title>
      <link>https://dev.to/allowfy_team</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/allowfy_team"/>
    <language>en</language>
    <item>
      <title>Blocking a QA agent: verify the sessions, then try the API</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Mon, 02 Nov 2026 08:04:54 +0000</pubDate>
      <link>https://dev.to/allowfy_team/blocking-a-qa-agent-verify-the-sessions-then-try-the-api-5gld</link>
      <guid>https://dev.to/allowfy_team/blocking-a-qa-agent-verify-the-sessions-then-try-the-api-5gld</guid>
      <description>&lt;p&gt;An agent-access switch is useful only if you check what happens beyond the switch. An administrator can see “Blocked” while still needing answers about existing sessions and credentials already issued to the agent.&lt;/p&gt;

&lt;p&gt;In our recorded gateway QA exercise, the practical response was to use the available stop controls and inspect their observable effects. This was a sandbox exercise with synthetic data, not an incident involving a real customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we checked after blocking the agent
&lt;/h2&gt;

&lt;p&gt;At the end of the exercise, the QA agent appeared as &lt;strong&gt;Blocked&lt;/strong&gt;. All five visible sessions appeared as &lt;strong&gt;Ended&lt;/strong&gt;. A subsequent request to retrieve a synthetic order returned &lt;code&gt;401 invalid_token&lt;/code&gt;; its error description said the token was unknown, expired, or revoked.&lt;/p&gt;

&lt;p&gt;Those are three distinct observations: the administrative decision, the session state, and the resource server's response. Together, they document the shutdown state that was actually reached. Reporting only the badge would have left the session and API checks out of the story.&lt;/p&gt;

&lt;p&gt;There is an important limit. The token was close to its expiry boundary. Client timestamps immediately before the request indicated that its issued lifetime had not yet elapsed, but this was not a controlled measurement of revocation propagation. We cannot isolate blocking from expiry well enough to claim instant invalidation or a particular revocation latency. The accurate result is that the agent was blocked, the five visible sessions were ended, and the later protected request was rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  A temporary pause was a different control
&lt;/h2&gt;

&lt;p&gt;Earlier in the same QA work, a gateway-wide pause had a more directly bracketed check. A listing request succeeded before the pause. While paused, a listing request and a reschedule-action request both returned &lt;code&gt;503 temporarily_unavailable&lt;/code&gt;, with a paused-access refusal. After access was resumed, listing succeeded again.&lt;/p&gt;

&lt;p&gt;This is a sanitized summary of those recorded checks, not a new test run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control state&lt;/th&gt;
&lt;th&gt;Request checked&lt;/th&gt;
&lt;th&gt;Observed result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before pause&lt;/td&gt;
&lt;td&gt;List synthetic appointments&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paused&lt;/td&gt;
&lt;td&gt;List synthetic appointments&lt;/td&gt;
&lt;td&gt;503, paused refusal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paused&lt;/td&gt;
&lt;td&gt;Request a reschedule action&lt;/td&gt;
&lt;td&gt;503, paused refusal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resumed&lt;/td&gt;
&lt;td&gt;List synthetic appointments&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After agent blocking&lt;/td&gt;
&lt;td&gt;Retrieve a synthetic order&lt;/td&gt;
&lt;td&gt;401, invalid token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pause and agent block were separate interventions. Resuming the gateway was not evidence of unblocking the agent. The rejected reschedule request did not perform a live appointment change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this taught us about a stop procedure
&lt;/h2&gt;

&lt;p&gt;A useful stop procedure checks administrative state, session state, and resource access separately. It also distinguishes a temporary operational pause from ending an individual agent's access. Those controls solve different operational problems and should not be reported as interchangeable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7009.html#section-3" rel="noopener noreferrer"&gt;OAuth token revocation guidance&lt;/a&gt; explains why invalidating already-issued credentials depends on token design and coordination between services. Our admin block exercise was not a test of the RFC's revocation endpoint or every related refresh token.&lt;/p&gt;

&lt;p&gt;To measure propagation properly in a future test, use a freshly issued credential with ample lifetime, an accepted baseline request, and timestamped checks across the relevant resource servers. That is a proposed extension, not coverage this exercise established.&lt;/p&gt;

&lt;p&gt;The working result here is deliberately narrower: the pause refused the two checked action paths and allowed a read after resumption; final shutdown inspection found the agent blocked, its visible sessions ended, and a later request rejected. No real commerce writes, payments, or refunds were performed.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This article is published by the Allowfy team and generated autonomously by AI from recorded Allowfy QA observations and the linked technical reference. These are sandbox observations, not a claim about production implementation or independently measured revocation timing.&lt;/p&gt;

</description>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Three different things people mean by verifying an audit log</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Sun, 01 Nov 2026 08:04:14 +0000</pubDate>
      <link>https://dev.to/allowfy_team/three-different-things-people-mean-by-verifying-an-audit-log-5f2f</link>
      <guid>https://dev.to/allowfy_team/three-different-things-people-mean-by-verifying-an-audit-log-5f2f</guid>
      <description>&lt;p&gt;“Verify the audit log” sounds like one task. In practice, it can mean at least three different checks: whether an application accepts a signed bundle, whether the chain of entries is internally consistent, and whether an independent verifier can validate the publisher’s signature. Those checks are related, but they are not interchangeable.&lt;/p&gt;

&lt;p&gt;Conflating them produces reports that sound stronger than the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the recorded QA run showed
&lt;/h2&gt;

&lt;p&gt;A recorded QA run of our gateway exercised the distinction. The application’s verifier accepted an earlier signed copy containing 12 entries and two signed checkpoints. A deliberately tampered earlier copy containing two entries was rejected at the first entry because its content/hash relationship no longer matched.&lt;/p&gt;

&lt;p&gt;Separately, an independent local analysis recomputed the available hash chain and digest successfully. That established that the entries and digest were internally reproducible under the observed format. It did not establish independent signature correctness: the exact Ed25519 signing message format was not available in the public bundle used for the check. &lt;a href="https://www.rfc-editor.org/rfc/rfc8032" rel="noopener noreferrer"&gt;RFC 8032&lt;/a&gt; defines Ed25519, but it does not define which application-specific ledger bytes this system signs.&lt;/p&gt;

&lt;p&gt;There was another visibility limit. The in-app verification view later reported a larger current ledger and a later head, but a subsequent export request returned a tiny response that was not validated as the final export. The older 12-entry file must therefore not be described as the final ledger. The application check and the independent recomputation answered different questions about different artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Name the verification claim precisely
&lt;/h2&gt;

&lt;p&gt;A useful verification report should label its subject and method:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;What it establishes&lt;/th&gt;
&lt;th&gt;What it does not establish&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application verifier accepted bundle&lt;/td&gt;
&lt;td&gt;The application accepted that bundle under its implemented checks&lt;/td&gt;
&lt;td&gt;Independent cryptographic validation or currentness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash chain recomputed&lt;/td&gt;
&lt;td&gt;Entries, links, and digest are consistent with the observed format&lt;/td&gt;
&lt;td&gt;Authenticity of the signer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature verified independently&lt;/td&gt;
&lt;td&gt;The verifier reconstructed the exact signing message and validated the public-key signature&lt;/td&gt;
&lt;td&gt;That the ledger is current, complete, or authorized by policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is an illustrative reporting model, not a claim about a specific product interface.&lt;/p&gt;

&lt;p&gt;For a robust design, publish a versioned verification specification. It should define canonical serialization, which bytes are hashed, how checkpoints commit to the log, the exact signature message, public-key identity and rotation rules, and how an export identifies its ledger head and completeness. A verifier should fail closed when required material is absent rather than silently downgrade “signature verified” to “hashes look consistent.”&lt;/p&gt;

&lt;p&gt;Currentness also deserves a first-class assertion. Bind an export to a server-declared head, generation, or cursor and expose whether the response is complete. If an export is empty, truncated, or not the expected content type, report delivery failure instead of presenting an older artifact as the latest one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build tests around artifacts and claims
&lt;/h2&gt;

&lt;p&gt;Test an intact bundle, a changed entry, a changed predecessor, a changed checkpoint, a reordered entry, a missing tail, a wrong public key, and a signature over a different canonicalization. Test old and current exports separately. Verify that the UI and API name the exact artifact, entry count, head, checkpoint status, and signature status being reported.&lt;/p&gt;

&lt;p&gt;A reviewer should be able to answer: Which file was checked? Which bytes were signed? Which parts were independently recomputed? Is this the latest complete export? If any answer is unknown, the result should say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;This evidence does not show a signature bug, a compromised ledger, or a lost customer record. It shows accepted and rejected artifacts, successful local recomputation, and an unavailable signing-message definition. It also does not certify that a later export was delivered. The general lesson is narrower and more useful: never use “verified” without naming the verifier, artifact, cryptographic claim, and freshness claim.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This article is published by the Allowfy team and generated autonomously by AI from recorded Allowfy QA evidence and technical references, then checked for consistency with those records. It is not a production case study; the underlying work was performed in a sandbox, not against production.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>testing</category>
    </item>
    <item>
      <title>A DPoP proof must use the key the token was issued for</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Thu, 29 Oct 2026 07:04:35 +0000</pubDate>
      <link>https://dev.to/allowfy_team/a-dpop-proof-must-use-the-key-the-token-was-issued-for-1hi2</link>
      <guid>https://dev.to/allowfy_team/a-dpop-proof-must-use-the-key-the-token-was-issued-for-1hi2</guid>
      <description>&lt;p&gt;DPoP is not satisfied merely because a request carries a signed proof. The proof must be signed by the key bound to the access token. That distinction matters because a client can produce a perfectly structured proof with a different key and still be presenting the wrong proof for that token.&lt;/p&gt;

&lt;p&gt;This article is about a concrete QA observation of that control. It is not a claim of a newly shipped fix or a production finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: possession of some key is not enough
&lt;/h2&gt;

&lt;p&gt;Use two symbolic keys: &lt;strong&gt;keyA&lt;/strong&gt; and &lt;strong&gt;keyB&lt;/strong&gt;. Suppose a token was issued with a binding to keyA. A client then sends that token with a DPoP proof created and signed using keyB. The request has a DPoP header, and the proof may be cryptographically well-formed, but it does not demonstrate possession of the key associated with the token.&lt;/p&gt;

&lt;p&gt;If a resource server accepted that combination, the token’s proof-of-possession boundary would be weakened. The relevant question is not only “is there a proof?” but also “is this the proof made with the token’s bound key?”&lt;/p&gt;

&lt;h2&gt;
  
  
  What the recorded QA did
&lt;/h2&gt;

&lt;p&gt;The recorded QA first established a signed-in appointment-listing baseline: the same &lt;code&gt;appointments.list&lt;/code&gt; POST returned HTTP 200. That successful response is useful context, but it is not the key-binding result by itself.&lt;/p&gt;

&lt;p&gt;A missing-proof negative control sent the token without a DPoP proof and received HTTP 401 with &lt;code&gt;invalid_dpop_proof&lt;/code&gt; and the description, “A single DPoP header is required.” This shows that the endpoint rejected the absence of the required proof.&lt;/p&gt;

&lt;p&gt;The key-binding case then generated an independent EC P-256 key, represented here as keyB, and called the same listing action with the token. The test code supplied that second key as the DPoP JWK. The service returned HTTP 401 with &lt;code&gt;invalid_dpop_proof&lt;/code&gt; and the specific description, “The proof's key is not the key this token is bound to.” That is the important observation: the client generated and signed a proof with a second key, and the service rejected the mismatch.&lt;/p&gt;

&lt;p&gt;The evidence does not establish an internal validation order, nor does it claim that signature verification independently passed as a separately observed stage. It records the externally visible enforcement result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The working control
&lt;/h2&gt;

&lt;p&gt;The observed control is token-to-proof key binding. A valid request needs more than a DPoP-shaped header: the proof key must correspond to the key for which the token was issued. The missing-proof case and the wrong-key case are complementary regression checks. The first catches an absent proof; the second catches a proof that is present but bound to the wrong key.&lt;/p&gt;

&lt;p&gt;This is deliberately not a replay analysis; replay behavior is a separate concern. It is also not an owner-authorization test. A request can use the correct proof key and still require independent authorization checks for the resource.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9449.html#section-4.3" rel="noopener noreferrer"&gt;RFC 9449 section 4.3&lt;/a&gt; describes resource-server validation of a DPoP proof, including matching the proof key to the key bound to the access token. &lt;a href="https://www.rfc-editor.org/rfc/rfc9449.html#section-7.1" rel="noopener noreferrer"&gt;RFC 9449 section 7.1&lt;/a&gt; explains the sender-constraining model. The proof protects the request context; it does not mean the request body is signed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bounded lesson
&lt;/h2&gt;

&lt;p&gt;When testing DPoP enforcement, keep a known-good 200 baseline, a missing-proof negative control, and a wrong-proof-key case. The strongest result is not a generic “401”; it is a refusal that identifies the token/proof key mismatch. That is what the recorded QA verified: an existing control rejected keyB when the token was bound to keyA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This article is published by the Allowfy team. AI-generated from recorded Allowfy QA observations and RFC 9449 references. The evidence was limited to the cited sandbox/source excerpts; no private key material or production claims are included, and no human authorship is implied.&lt;/p&gt;

</description>
      <category>security</category>
      <category>oauth</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Succeeded in test mode does not mean the order changed</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:10:57 +0000</pubDate>
      <link>https://dev.to/allowfy_team/succeeded-in-test-mode-does-not-mean-the-order-changed-36h9</link>
      <guid>https://dev.to/allowfy_team/succeeded-in-test-mode-does-not-mean-the-order-changed-36h9</guid>
      <description>&lt;p&gt;A green-looking operation result can describe a completed test of a workflow, not a completed change to an order. That distinction matters when a UI uses the same word—“Succeeded”—for a dry run and for a real write.&lt;/p&gt;

&lt;p&gt;This is a narrow lesson from a documented QA exercise of our gateway. It is not a claim about a production payment or fulfillment system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the QA exercise actually showed
&lt;/h2&gt;

&lt;p&gt;The gateway was still in Test mode. An attempt to enable live operation returned HTTP 402; the available evidence did not establish the reason. The UI closed without an explanatory alert and remained in Test mode. No live store write followed that attempt.&lt;/p&gt;

&lt;p&gt;The QA case then exercised two customer-approved operations against synthetic store fixtures: cancellation of one order and return of another. Both proposals carried &lt;code&gt;test: true&lt;/code&gt;. The cancellation confirmation returned a successful result and explicitly said that nothing was changed. The return confirmation did the same. A repeated terminal confirmation returned the same result rather than creating a second visible completion.&lt;/p&gt;

&lt;p&gt;The useful observation was not “the cancellation worked” or “the refund worked.” It was: the gateway completed its Test mode decision path and reported the outcome of that path. Store readback remained unchanged afterward. The relevant fixtures still showed their pre-operation states, with no refund amount added. The return wording asked the store to return the item; it did not prove that a refund had been executed.&lt;/p&gt;

&lt;p&gt;The completed-order condition in the return fixture was deliberate setup in the synthetic QA environment. It was used to make a delivered-order return scenario available for testing. It did not represent actual shipping. There were no real payments, refunds, shipping-provider calls, or customer transactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical QA reading of “Succeeded”
&lt;/h2&gt;

&lt;p&gt;When a result is marked successful, check at least four separate facts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Evidence needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Did the gateway accept the requested workflow?&lt;/td&gt;
&lt;td&gt;Operation result and policy/approval record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Was it a dry run?&lt;/td&gt;
&lt;td&gt;An explicit Test mode or &lt;code&gt;test: true&lt;/code&gt; indicator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the external system change?&lt;/td&gt;
&lt;td&gt;Fresh readback of the target order or record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did money or fulfillment move?&lt;/td&gt;
&lt;td&gt;Provider-side evidence, not merely a gateway message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are different assertions. A successful proposal, approval, or simulated completion cannot substitute for external readback. A UI label is especially unsafe as the sole source of truth when the same label can appear on a test operation.&lt;/p&gt;

&lt;p&gt;For QA, record the mode alongside every result, preserve the before-and-after readback, and make the no-change statement visible in the report. If the system offers a live transition, verify the resulting mode independently before testing a mutating path. If that transition fails silently, report the missing explanation separately from the status code; do not infer a billing cause that the response body did not establish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this evidence
&lt;/h2&gt;

&lt;p&gt;These observations do not certify live execution, idempotency under worker concurrency, payment settlement, refunds, shipping, or a production rollback. They also do not prove that every Test mode integration behaves identically. They establish only that these recorded synthetic operations completed as dry runs and that the available store readback stayed unchanged.&lt;/p&gt;

&lt;p&gt;For reference, see &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html" rel="noopener noreferrer"&gt;RFC 9110&lt;/a&gt; for HTTP semantics; it does not turn an application’s “Succeeded” label into proof of a resource mutation.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This article is published by the Allowfy team. AI-generated from recorded Allowfy QA observations and technical references; no human authorship is implied, and this is not a production implementation claim.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
    </item>
    <item>
      <title>A repeated confirmation is not an exactly-once guarantee</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Tue, 27 Oct 2026 07:07:37 +0000</pubDate>
      <link>https://dev.to/allowfy_team/a-repeated-confirmation-is-not-an-exactly-once-guarantee-4poc</link>
      <guid>https://dev.to/allowfy_team/a-repeated-confirmation-is-not-an-exactly-once-guarantee-4poc</guid>
      <description>&lt;p&gt;Seeing the same successful response twice can be reassuring, but it is not the same as proving exactly-once execution. A terminal response may be idempotent at the API surface while the underlying system still needs separate evidence for durable writes, retries, and recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the documented QA run observed
&lt;/h2&gt;

&lt;p&gt;In a documented QA run of our gateway, repeating a confirmation after the operation was terminal returned the same &lt;code&gt;succeeded&lt;/code&gt; result and the same &lt;code&gt;confirmed_at&lt;/code&gt; value. The result was marked &lt;code&gt;test:true&lt;/code&gt;. An earlier 12-entry fixture export also showed one &lt;code&gt;operation.confirmed&lt;/code&gt; event and one &lt;code&gt;operation.succeeded&lt;/code&gt; event for that fixture.&lt;/p&gt;

&lt;p&gt;Those observations support a precise statement: the tested terminal retry presented the same result, and the available earlier event fixture contained one recorded confirmation and one recorded success. They do not support a stronger claim that the system provides an exactly-once guarantee under every failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotent response versus exactly-once work
&lt;/h2&gt;

&lt;p&gt;A retry-safe endpoint can recognize an already-completed operation and return its stored terminal result instead of creating another visible transition. That is valuable. But exactly-once execution is a much broader systems property. A timeout can happen after a downstream write and before the caller receives a response. A process can restart between steps. A worker can receive the same message twice. A database commit and an external side effect can fail to become one atomic transaction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2" rel="noopener noreferrer"&gt;RFC 9110’s discussion of idempotent methods&lt;/a&gt; is a useful boundary: idempotence helps a client safely repeat a request when it cannot tell whether the prior attempt succeeded, but it does not make every unsafe &lt;code&gt;POST&lt;/code&gt; automatically retryable. An application still needs an operation identity, durable state, and a deliberate retry contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical design and test advice
&lt;/h2&gt;

&lt;p&gt;Persist a stable operation key and bind it to the action, resource, approved revision, and relevant principal. On a repeat, return the stored result—or a clearly defined in-progress response—without inventing a new confirmation. Record terminal timestamps and event identities so operators can inspect whether one logical transition was recorded.&lt;/p&gt;

&lt;p&gt;Then test beyond the happy terminal retry: retry after a client timeout, retry after a worker restart, duplicate delivery, and failure between internal persistence and an external side effect. Inspect durable state after each case. If the system cannot make the whole effect atomic, document the boundary and provide reconciliation or an explicit unknown-outcome path rather than calling the behavior exactly once.&lt;/p&gt;

&lt;p&gt;A sanitized diagnostic sequence is enough for a manuscript: &lt;code&gt;confirm → succeeded, confirmed_at=T&lt;/code&gt;; &lt;code&gt;repeat confirm → same succeeded, confirmed_at=T&lt;/code&gt;; &lt;code&gt;earlier fixture → one confirmed, one succeeded&lt;/code&gt;. It is not a substitute for a concurrency or crash test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this observation
&lt;/h2&gt;

&lt;p&gt;The recorded run did not test concurrency, process restart, worker duplication, timeout-after-write, or external side-effect failure. The 12-entry fixture was an earlier export, not a final ledger export. The sandbox used isolated mock commerce data, no real funds, and no real refunds or customer transactions. The result is evidence of terminal-repeat behavior in that fixture, not an exactly-once production claim.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This article is published by the Allowfy team. Generated using AI from recorded Allowfy QA observations and linked technical references; sandbox results, not a production implementation claim.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>Customer approval and business review are different state transitions</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:09:25 +0000</pubDate>
      <link>https://dev.to/allowfy_team/customer-approval-and-business-review-are-different-state-transitions-p7h</link>
      <guid>https://dev.to/allowfy_team/customer-approval-and-business-review-are-different-state-transitions-p7h</guid>
      <description>&lt;p&gt;A customer saying “yes” and a business reviewer approving an operation answer different questions. Treating them as one approval state makes it hard to tell who authorized the next step, what remains pending, and which actor may move the workflow forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the documented QA run observed
&lt;/h2&gt;

&lt;p&gt;In a documented QA run of our gateway, the owner-review fixture moved through a recorded sequence: &lt;code&gt;proposed&lt;/code&gt;, then after user confirmation &lt;code&gt;in_progress&lt;/code&gt; with &lt;code&gt;awaiting business review&lt;/code&gt;, then team approval, and finally &lt;code&gt;succeeded&lt;/code&gt; with &lt;code&gt;test:true&lt;/code&gt;. The exercise was performed in Test mode. It showed a customer confirmation followed by a separate business-review transition, rather than a single confirmation immediately authorizing completion.&lt;/p&gt;

&lt;p&gt;That distinction is important in the evidence. The fixture’s success does not demonstrate that an arbitrary reviewer could approve it. No unauthorized-reviewer test was performed, and the run should not be described as one. The observed claim is only that this owner-review path recorded the listed states and reached a simulated success after team approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the workflow as separate decisions
&lt;/h2&gt;

&lt;p&gt;Customer approval usually answers: “Does the customer authorize these presented terms for this action?” Business review answers: “Has the organization completed its required internal review under its own policy?” Those decisions can have different actors, permissions, evidence, and expiration rules.&lt;/p&gt;

&lt;p&gt;A useful state model therefore keeps the transitions explicit. A customer confirmation can move a proposed operation to an internal-review queue. A permitted reviewer can move that queue item to approved. Only an authorized execution path should move an approved operation to succeeded. &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Transaction_Authorization_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP’s Transaction Authorization Cheat Sheet&lt;/a&gt; similarly emphasizes clear transaction state transitions and checking authorization at the point where the business action is actually executed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical design and test advice
&lt;/h2&gt;

&lt;p&gt;Store separate audit facts for customer approval and business approval: actor type, identity reference, approved revision, policy decision, timestamp, and resulting state. Define which roles may perform each transition and reject attempts from roles that do not qualify. Test the happy path, a customer approval without business approval, a business approval before customer approval, a stale approval, and a terminal-state repeat. Also test that the UI does not offer a review control when an operation is already terminal if the product does not support that transition.&lt;/p&gt;

&lt;p&gt;For documentation, a sanitized text sequence is clearer than a screenshot or raw export: &lt;code&gt;proposed → customer confirmed → awaiting business review → team approved → succeeded (test)&lt;/code&gt;. It says what was observed without implying that every reviewer or every production workflow has the same authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this observation
&lt;/h2&gt;

&lt;p&gt;The exercise used an isolated sandbox and did not move real money, refund a real order, or perform a live commerce write. It did not test an unauthorized reviewer, concurrent approvals, restarts, reconciliation, or a production authorization transaction. The state sequence is evidence for this recorded owner-review fixture—not proof that business-review enforcement is complete across all roles and integrations.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Generated using AI from recorded Allowfy QA observations and linked technical references; sandbox results, not a production implementation claim.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>testing</category>
      <category>api</category>
    </item>
    <item>
      <title>Approval belongs to a revision, not just an operation ID</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Sun, 25 Oct 2026 07:06:46 +0000</pubDate>
      <link>https://dev.to/allowfy_team/approval-belongs-to-a-revision-not-just-an-operation-id-2o08</link>
      <guid>https://dev.to/allowfy_team/approval-belongs-to-a-revision-not-just-an-operation-id-2o08</guid>
      <description>&lt;p&gt;A customer approval is meaningful only if the system can show what was approved. An operation ID identifies a workflow, but it does not by itself prove that a later confirmation still matches the terms the customer saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the documented QA run exercised
&lt;/h2&gt;

&lt;p&gt;In a documented QA run of our gateway, a cancellation was represented as a proposed operation in Test mode. The recorded exercise tried a standing-permission confirmation and received &lt;code&gt;403 user_approval_required&lt;/code&gt;. It then tried a confirmation against revision 2 and received &lt;code&gt;409 terms_changed&lt;/code&gt;. The successful path used the correct user approval for revision 1 and returned &lt;code&gt;200&lt;/code&gt;; the response explicitly said that Test mode made no change.&lt;/p&gt;

&lt;p&gt;That sequence is useful because it distinguishes three decisions that can otherwise blur together: whether a standing permission is sufficient, whether the submitted revision is current, and whether the customer’s approval is valid for that revision. The observed record does not establish that anyone edited the cancellation terms between revisions. The important fact is narrower: the system rejected the revision-2 confirmation and accepted the customer-approved revision-1 path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the distinction matters
&lt;/h2&gt;

&lt;p&gt;A confirmation endpoint that accepts only an operation ID can accidentally turn “I approved these terms” into “I approve whatever this operation means now.” That is risky when amount, target, timing, policy result, or downstream action can change. A safe design binds approval to a server-held representation of the terms—or to a revision with an equivalent integrity check—and validates that binding again at the final execution gate.&lt;/p&gt;

&lt;p&gt;This is the same practical idea behind the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Transaction_Authorization_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Transaction Authorization Cheat Sheet&lt;/a&gt;: show the user what will be signed, invalidate authorization when relevant data changes, and enforce the check at the business execution step. The revision is not a cosmetic counter. It is part of the authorization boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical design and test advice
&lt;/h2&gt;

&lt;p&gt;Record the approved revision alongside the principal, resource, action, tenant, policy context, and expiration. On confirmation, reject a missing or stale revision rather than silently selecting the latest one. Test at least these paths: standing permission where an explicit approval is required; an approval for an older revision; the current user-approved revision; and a repeated terminal confirmation. Keep response meanings distinct so clients can tell “approval required” from “terms no longer match.”&lt;/p&gt;

&lt;p&gt;A diagnostic sequence can be written as sanitized text, not a raw export: &lt;code&gt;proposed → approval required&lt;/code&gt;, &lt;code&gt;revision mismatch → terms changed&lt;/code&gt;, &lt;code&gt;approved revision → succeeded (Test mode)&lt;/code&gt;. That makes the contract reviewable without exposing operation IDs or customer data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this observation
&lt;/h2&gt;

&lt;p&gt;This was an isolated mock WooCommerce and gateway exercise in a sandbox, with no real funds and no real commerce write. It did not prove that terms were edited between versions, nor did it test concurrent confirmations, process restarts, database-write races, or a production execution transaction. The successful result demonstrates the recorded QA path, not a universal guarantee for every integration.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Generated using AI from recorded Allowfy QA observations and linked technical references; sandbox results, not a production implementation claim.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>testing</category>
      <category>api</category>
    </item>
    <item>
      <title>A valid agent token isn't permission to cancel every order</title>
      <dc:creator>allowfy</dc:creator>
      <pubDate>Sat, 24 Oct 2026 15:47:28 +0000</pubDate>
      <link>https://dev.to/allowfy_team/a-valid-agent-token-isnt-permission-to-cancel-every-order-98</link>
      <guid>https://dev.to/allowfy_team/a-valid-agent-token-isnt-permission-to-cancel-every-order-98</guid>
      <description>&lt;p&gt;An agent can have a valid token and still ask to change the wrong order.&lt;/p&gt;

&lt;p&gt;Imagine Alice authorizes an agent to manage her orders. The agent sends a cancellation request, but the order ID belongs to Bob. A token signature check can succeed. So can a proof-of-possession check. Neither makes Bob's order Alice's.&lt;/p&gt;

&lt;p&gt;This is a useful boundary to test before connecting an agent to a write endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery, grants, and policy answer different questions
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://personalagentprotocol.org/docs/0.2/spec" rel="noopener noreferrer"&gt;Personal Agent Protocol, or Poppy&lt;/a&gt;, &lt;code&gt;/.well-known/poppy.json&lt;/code&gt; tells an agent how to connect. Poppy also covers sign-in, sessions, scopes, and interfaces; it is not just that discovery file. The specification is currently a draft.&lt;/p&gt;

&lt;p&gt;OAuth grants scoped access. The application still has to enforce what that grant means for the requested resource and action.&lt;/p&gt;

&lt;p&gt;DPoP addresses a different problem: constraining token presentation to a client that can prove possession of the corresponding private key. It does not decide whether that client may cancel this particular order. Standard DPoP also does &lt;strong&gt;not&lt;/strong&gt; sign the request body; see &lt;a href="https://www.rfc-editor.org/rfc/rfc9449.html#section-11.7" rel="noopener noreferrer"&gt;RFC 9449, section 11.7&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A request can therefore pass the protocol checks and fail the application's authorization policy. That is expected, not a protocol failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make one policy small enough to test
&lt;/h2&gt;

&lt;p&gt;For this example, the rule is deliberately narrow: a customer may cancel only their own pending orders, inside their tenant, with write access.&lt;/p&gt;

&lt;p&gt;The inputs below are &lt;strong&gt;trusted server-side objects&lt;/strong&gt;. &lt;code&gt;Principal&lt;/code&gt; comes from verified authentication/session middleware and a server-side mapping to the customer and tenant. &lt;code&gt;Order&lt;/code&gt; comes from the database, not ownership fields supplied in the request body. Constructing these dataclasses from arbitrary client JSON would defeat the example.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;poppy:write&lt;/code&gt; is a Draft 0.2 scope. The code is a policy example, not a Poppy implementation or conformance test.&lt;/p&gt;

&lt;p&gt;Save this as &lt;code&gt;policy_example.py&lt;/code&gt; and run it with Python 3.10 or later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Educational policy example, not production authentication code.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Principal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Inputs come from trusted authentication and server-side order loading.
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poppy:write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Principal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shop-a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poppy:write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="n"&gt;own_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shop-a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner with write access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read-only grant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poppy:read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})),&lt;/span&gt; &lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no grant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;another customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;another tenant, same customer ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shop-b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already shipped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;own_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unrecognized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;can_cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; policy checks passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is &lt;code&gt;8 policy checks passed&lt;/code&gt;. Notice the cross-tenant case deliberately keeps the customer ID unchanged. An ID that matches inside one tenant must not accidentally identify the same customer in another.&lt;/p&gt;

&lt;p&gt;The owner comparison is appropriate only for this example's owner-only policy. Shared accounts, staff roles, and delegated access need their own relationship checks. This is not a universal fix for object-level authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these checks do not prove
&lt;/h2&gt;

&lt;p&gt;These are local policy tests, not HTTP integration tests. They do not validate tokens, issuer/audience, expiry or revocation, DPoP proofs, replay handling, or TLS. Those controls belong upstream and need separate tests.&lt;/p&gt;

&lt;p&gt;A boolean decision is also not a safe execution transaction. Recheck mutable authorization and order state at the commit boundary, using an appropriate transaction or conditional write. If your policy requires approval for a specific action, validate a server-held approval bound to the principal/client, tenant, order, action, and current revision. A request field like &lt;code&gt;approved: true&lt;/code&gt; is not that approval.&lt;/p&gt;

&lt;p&gt;For a real endpoint, repeat the negative cases through the HTTP layer: change the order ID, switch the tenant context, use read-only credentials, and retry after the order changes state. &lt;a href="https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/" rel="noopener noreferrer"&gt;OWASP API1:2023&lt;/a&gt; explains why authorization must check the requested action on the requested object.&lt;/p&gt;

&lt;p&gt;The useful question is not only “Is this token valid?” It is “Does this principal have permission to perform this action on this resource, in its current state?”&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Published from the Allowfy team account. This article was generated using AI tools and checked against the linked technical references. The Python sample was executed locally. It is an educational example, not a description of Allowfy's implementation.&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>ai</category>
      <category>api</category>
    </item>
  </channel>
</rss>
