Repository navigation
test(security): pin tenant route permissions and audit every operator user change - #22
Merged
Merged
Conversation
RouteSecurityTest only required exactly one TenantPermission per tenant route, so changing a route to another permission that every role holds today passed every test. The new test pins route name to permission over the whole tenant route set, and a negative control weakens the member detail route from membership.list to organization.view.
…without their events user.disabled, user.enabled and user.password_reset were never asserted by action name, and enabling, user creation, organization creation and the operator membership add had no audit-failure test. AuditTrailTest now checks the action, the operator actor and the absence of secrets for the three user changes, and that each of the other commands stores nothing while its audit event cannot be written. Two negative controls remove the user.enabled audit write and move enabling out of the audit transaction.
The authentication guide's test table now names the per-route permission pin of RouteSecurityTest and the operator commands that AuditTrailTest checks with a failing audit write.
This was referenced Oct 24, 2026
…controls Seven negative controls for protections on main that no control covered: URL fragments under URL keys, the referer key, values of sensitive keys, database driver messages, the failed-login pseudonym folding (#23), and the member list order and total. Two tests close the gaps the fragment and pseudonym controls exposed. The documented count becomes 48.
Only the documented negative-control count conflicted: 53 with #26's three controls.
This was referenced Oct 25, 2026
knittingcode
added a commit
that referenced
this pull request
Oct 25, 2026
knittingcode
added a commit
that referenced
this pull request
Oct 26, 2026
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.
Summary
Adds tests and negative controls to main for protections that had none. No production code changes.
Motivation
Read-only gap analyses of the security tests found regressions that no test on main would catch:
#[TenantPermission]to a weaker permission passed every test.flowcrm:user:enable, or enabling outside the audit transaction, passed every test.Changes
RouteSecurityTest::testEveryTenantRouteDeclaresItsPinnedPermission: pins the exact permission of every tenant route on main:api_v1_organization→ organization.view;api_v1_organization_memberandapi_v1_organization_members→ membership.list.Adding a tenant route or changing its permission fails the test, so the map must be updated on purpose.
AuditTrailTest::testOperatorUserChangesAreAuditedWithTheirAction: disable, enable and reset-password each writeuser.disabled,user.enabledanduser.password_reset. The test checks:platform_operator;AuditTrailTest::testOperatorEnableAndProvisioningWhoseAuditEventCannotBeWrittenChangeNothing: while the audit write fails, these commands store nothing:flowcrm:user:enable;flowcrm:user:create;flowcrm:organization:create;flowcrm:membership:add.The existing helper
whileAuditFailsFor()now delegates to a newwhileAuditFails($when), with no change in behaviour.SecurityLoggingTest::testFailedLoginsOfOneAccountShareOnePseudonymWhateverTheSpelling: two failed logins, one with the address as stored and one upper-cased withU+0130, must log the sameaccount_ref. The lookup folds both to the same account, so the log must too.JsonLineFormatterTest::testEveryUrlKeyLosesTheFragmentOfAUrlWithoutAQuery:/accounts#search=…under every URL key must lose the fragment.Ten new negative controls in
controls.php, each next to the related entries:Docs: the BE-18 control count is 48 (main's 38 plus these 10) in
authentication.md, ADR-019 anddev-environment.md, which still said 28. The authentication guide's test table names the pinned permissions, the operator commands checked with a failing audit write, and the pseudonym folding. The observability test table names the fragment case.Testing
d3f93f3:OK (274 tests, 2466 assertions);SecurityLoggingTest|JsonLineFormatterTest:OK (68 tests, 479 assertions);run-controls.php: 48 of 48 negative controls detected.Interaction with other PRs
RouteSecurityTest::TENANT_PERMISSIONS: addapi_v1_organization_member_add→MembershipAdd,api_v1_organization_member_change_role→MembershipChangeRole,api_v1_organization_member_remove→MembershipRemove, betweenapi_v1_organization_memberandapi_v1_organization_members. This file merges without a textual conflict, so the pin test fails if it is forgotten.AuditTrailTest.php: take feat(organizations): membership administration API (FND-010C) #19's side (shared helper trait), then pass$disabled->idtowhileAuditFailsFor().authentication.mdanddev-environment.md: count the mergedcontrols.php(grep -c "^ \['") rather than adding the numbers; CI must show N of N. Verified for feat(organizations): membership administration API (FND-010C) #19 at1565f72and this branch atb92ba2b: 71 controls, all detected, integrationOK (313 tests).61c3d94) and merged into this branch (b92ba2b): the documented count is 50 (38 + fix(auth): match no account for an e-mail address with a NUL byte #28's 2 + these 10); the authentication test table keeps both this branch's text and fix(auth): match no account for an e-mail address with a NUL byte #28's console input rule. Locally:50 of 50 negative controls detected.e03d151) and merged into this branch (2ce29c7): the documented count is 53 (main's 43 and these 10). Locally and in CI 40186922451: integrationOK (278 tests),53 of 53 negative controls detected. With feat(organizations): membership administration API (FND-010C) #19 as well, the total is 74 (verified in a scratch simulation of the whole queue one03d151).controls.php. These ones are inserted elsewhere to keep conflicts small.Known limitations