Repository navigation
feat(authorization): add allOf/anyOf combinators to role grants - #1071
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d logic allOf / anyOf grants are decided by one Kleene matcher (RoleGrantMatcher) shared by the instance-bound RoleGrantEvaluator and the static twin EvaluateRolesStatic; the two differ only in how a leaf matches. A role-bound leaf is Unknown for a role-less caller, identity leaves are Yes/No; a deny fires on Yes or Unknown, an allow admits only on Yes. The previous-transition prefetch now looks at every leaf, and the interim combinator guards from the model change are gone (AuthorizeAppService.ToRoleGrantDtos keeps its own). Behaviour change for plain grants: a role-less caller no longer satisfies an ALLOW $role. grant whose path resolves to "". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntity The leaf hop runs in an isolated scope with no ambient caller, so $InstanceStarter / $PreviousUser grants never matched. The caller's actor and subject now travel in HumanTaskLeafRequest and are evaluated against the leaf instance's own history through a CallerIdentity overload of CreateEvaluatorAsync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A SubFlow is part of its parent's process: for an instance with an active SubFlow the queryRoles decision is the deepest active leaf's, not an AND down the chain. Consequence: the root's queryRoles no longer restrict anyone while the instance is inside a SubFlow; an empty leaf set allows; a parent restricts via subFlow.overrides.states.<state>.queryRoles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…af-only rules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The combinator parser deserializes grants directly, which kept padded role names; a padded deny then no longer matched its role and the field became visible to it. Trim after deserializing, for plain grants and combinator children. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly authorize Add migration entries for leaf-only queryRoles and the new publish rejections, a known issue for the mixed-version/rollback hazard, and the 0.0.99 behavior-change bullets. Drop plan decision codes from committed files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Correct the leaf resolver remark, read the caller identity once per request in the human-task list, and pin an instance-bound multi-role allOf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…face Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2 findings · reviewed 🟠 Unauthenticated leaf route trusts caller identity fields ·
|
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
Overall Grade |
Security Reliability Complexity Hygiene |
Code Review Summary
| Analyzer | Status | Updated (UTC) | Details |
|---|---|---|---|
| C# | Oct 4, 2026 2:27p.m. | Review ↗ |
Important
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
|




Summary
RoleGrantgains one-levelallOf/anyOf: a grant isroleXORallOfXORanyOf, children are{ "role" }only. Applies to workflow/statequeryRoles, transitionroles,availableIn, functionroles, long-poll roles, subflow overrides and schemax-roles(closes [enhancement] Add allOf/anyOf combinators to RoleGrant #1057).RoleGrantMatcher) with three-valued logic shared by the instance-bound evaluator andEvaluateRolesStatic: a role-bound leaf is Unknown for a role-less caller; a deny fires on Yes or Unknown, an allow admits only on Yes. Plain single-role grants behave exactly as before.authorize?queryRoles=truenow decides at the deepest active SubFlow leaf only (parent-stamped override ?? leaf state ?? leaf workflow); the root/intermediate AND is removed. Parent-owned transitions and?ack=trueare unchanged.act_sub/sub, andCallerScopeHashnow includessub.Worked example (read this first):
docs/domain/role-grant-walkthrough.md— one definition, eight callers, verdict tables forauthorize, the state function,x-roles, the human-task list, SubFlow leaf decisions, overrides and the long-poll acknowledge.Changes
RoleGrant(+RoleGrantCondition,LeafRoles),RoleGrantDefinitionRules(publish rules),SchemaRolesParser(combinator x-roles, trimmed as before; exemption lists stay single-role).GrantMatch/RoleGrantMatcher,RoleGrantEvaluator,TransitionAuthorizationManager(prefetch walks leaves;CreateEvaluatorAsyncoverload takingCallerIdentity),AuthorizeAppService(leaf-onlyqueryRoles; matrix DTOallOf/anyOf), human-task contracts/resolver,CallerScopeHash, Flow/Function/Schema component validators.role-grant-authorization.md(Combinators, Behavior changes in 0.0.99),authorize-function.md,human-task-function.md, newrole-grant-walkthrough.md;vnext-metafeatures / security-policy / migrations / known-issues.Test Plan
dotnet test test/BBT.Workflow.Application.Tests --filter "FullyQualifiedName~Authoriz|FullyQualifiedName~RoleGrant|FullyQualifiedName~HumanTask|FullyQualifiedName~Schema"— only pre-existing master failuresdotnet test test/BBT.Workflow.Domain.Tests --filter "FullyQualifiedName~RoleGrant|FullyQualifiedName~SchemaRoles|FullyQualifiedName~Validator" --blame-hang-timeout 30s --blame-hang-dump-type noneRoleGrantCombinatorTests/AuthorizeAppServiceSubflowTestsIntegration test evidence
vnext-exampleTests/AuthorizationChainLab,Tests/RoleMatrixLab,Tests/HumanTaskChain(test(authorization): cover role-grant combinators and leaf-only authorize vnext-example#31)VNEXT_BASE_URL=http://localhost:4201(run-docker.sh up core), 2026-10-03CombinatorGrantTests4/4 green; the 23 reds are the already-recorded known set (gateway-ownedqueryRoles16, function mapping CS8197 5,$InstanceStarterwithoutact_sub2)Connection refused :4211,Discovery:700002). The cross-domain identity hop is not yet verified (needs the four-domain lab)CreatedBy=u-ali,CreatedByBehalfOf=c-acme; the 9ht_cincidents are allDiscovery:700002TEST-SCENARIOS.mdrows updated: yesNotes
allOf/anyOf.vnext-meta/migrations.json):queryRolesloosens access where a root declaresqueryRolesbut a SubFlow leaf has none — addsubFlow.overrides.states.<state>.queryRolesor leafqueryRoles;x-rolesentries and functionroleswith a malformed dynamic path are now rejected at publish (they were silently inert before);$role.grant whose path resolves to "".known-issues.json): author the first combinator only after every pod — and every domain that stamps overrides onto this one — runs this release; do not binary-downgrade once combinators are published.CallerScopeHashchange re-keys the state/data/schema/human-task caches once at deploy (one round of 200s instead of 304s).🤖 Generated with Claude Code