[Phase 3] Add ACL fields to Context type and wire into lifecycle hooks #16
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_osis#16
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The OSIS Context type currently has no access control fields. For multi-tenant deployments (e.g., znzfreezone with per-reseller contexts), we need ACL fields on the Context itself so that lifecycle hooks can enforce who is allowed to read/write/admin each context.
Background
In the znzfreezone architecture:
freezone_admincontext: authoritative records, admin-onlyfreezone_sharedcontext: request/approval queue, admin + resellers can writefreezone_reseller_{id}contexts: per-reseller private workspacesCurrently, access control is enforced entirely in
znzfreezone_backend'srpc_auth.rsmiddleware. This works but means every application using hero_osis multi-context must re-implement context-level ACL. It should be a first-class OSIS feature.Note:
hero_rpcalready has a full ACL module (crates/osis/src/acl.rs, ~820 lines) with hierarchical Rights (Admin/Write/Read), Group-based membership, and recursive resolution — but it is not wired into the dispatch pipeline.Proposed Changes
1. Add ACL fields to Context type
In
base/core/types.oschema, add to the Context type:These get auto-generated into
base/types_generated.rsby the oschema codegen.2. Wire ACL checks into lifecycle hooks
The lifecycle hook stubs in
hero_osis_server(before_create,before_update,before_delete,before_read) should check the caller's identity against the context's ACL fields.This requires the caller identity to be passed through the RPC dispatch pipeline (see related hero_rpc issue for
RequestContext).3. Context creation with ACL initialization
When a new context is registered via
ContextLifecycleRequest, the creator's public key should be set asowner_pubkeyand added toacl_admin_pubkeysby default.Integration with hero_rpc ACL module
The existing
hero_rpcACL module (acl.rs) provides:AclEntrywith circle/group resolutionRightsenum (Admin, Write, Read)check_access(identity, required_right)logicConsider whether to:
Recommendation: Start with Option A (simple pubkey lists) and migrate to Option B later if group-based ACL is needed.
Related
znzfreezone_code/znzfreezone_backendissue #29: Cross-context authoritative signatureslhumina_code/hero_rpc: Needs RequestContext to pass caller identity (separate issue)lhumina_code/hero_proxy: Identity header injection (separate issue)hero_rpcACL module atcrates/osis/src/acl.rsUpdate: Keypair as Primary Identity Primitive
Following discussion on hero_proxy#8, authentication is moving to hero_proxy with keypair-only auth for programmatic access (API keys are being dropped).
This means the ACL fields on Context should use secp256k1 public keys as the identity primitive:
The caller identity comes from hero_proxy's
X-Proxy-User-Pubkeyheader (injected after signature verification), which flows through hero_rpc'sRequestContext(hero_rpc#11) into lifecycle hooks.This aligns with the simplified auth model:
i think we should have a single field called acl which is a list of access control entries. then an entry can have an audience (list of groups + identity ids) and permissions.
yes.
as for integration:
Analysis: ACL as a first-class OSIS domain
Explored the codebase to map out what exists and what needs to change. Here's the full picture.
What exists today
hero_rpc
crates/server/src/acl/mod.rs(~820 lines) has a solid in-memory ACL implementation:Rightenum:Admin > Write > Read(hierarchical implication)Group: named collection of users (by pubkey) + nested groups (by name), recursive membership resolution with circular-reference protectionAce: links aRightto a list of group namesAcl: groups map + entries list, withhas_right(pubkey, right),get_highest_right(pubkey),get_user_groups(pubkey)hero_osis already has ACL-adjacent schemas:
identity/group.oschema—Grouprootobject withGroupMember(user_id +GroupRole: reader/writer/admin/owner), subgroups, parent_group, hierarchyledger/groups.oschema—Groupsservice withMemberId(Account|Group),Role(Viewer/Contributor/Member/Coordinator), admin management, circular membership preventionledger/kvs.oschema— namespace-scoped admin/writer ACL patternCurrent Context type (
base/core.oschema) is minimal — justsid,name,created_at,updated_at. No ACL fields.Proposed approach: single
aclfield on ContextPer comment #15695 — instead of flat pubkey lists, Context gets a single structured
aclfield. This maps directly to the hero_rpc ACL model, expressed as OSchema types:Then Context gains:
This gives us:
aclfield = list of entries, each with audience (groups + identities) and permission levelAclGroupas rootobject = groups are persisted via OSIS CRUD, auto-generatedacl_group_get(),acl_group_set(),acl_group_list(), etc.Making ACL a domain in hero_osis
Two options for where the ACL types + service live:
Option A: Extend base domain — add
acl.oschematoschemas/base/. The base domain already owns Context, so ACL types naturally belong here. AclGroup becomes a rootobject alongside Context, Server, Template.Option B: New
acldomain —schemas/acl/acl.oschema. Cleaner separation, own database file per context (acl.db), own service handler. But creates a cross-domain dependency since Context (base) references AclEntry (acl).Recommendation: Option A (extend base domain). Context already lives in base, and AclEntry is an embedded type (not a rootobject), so it's just a nested struct on Context. Only AclGroup needs to be a rootobject (for group CRUD). This avoids cross-domain references.
ACL service methods
Beyond auto-generated CRUD, we need an
AclServicewith business logic:The implementation in
base/rpc.rsreuses the resolution logic from hero_rpc'sAcl::get_user_groups()andAcl::has_right(), but reads groups from OSIS storage instead of in-memory structs.Wiring into lifecycle hooks
The existing trigger pattern (
*_trigger_get_pre,*_trigger_save_pre,*_trigger_delete_pre) returnsbool— perfect for ACL enforcement. The question is how the caller's pubkey reaches the hook.This depends on hero_rpc's
RequestContext(hero_rpc#11). Once that lands, the flow is:X-Proxy-User-PubkeyheaderRequestContext.caller_pubkeyRequestContextto lifecycle hooksAclService::check_access(caller_pubkey, Right::Write)→ allows/deniesThe hook signatures would need to change from
fn trigger_save_pre(obj: &mut T) -> boolto something likefn trigger_save_pre(obj: &mut T, ctx: &RequestContext) -> bool. This is a codegen change in the OSIS server generator.Migration path for hero_rpc ACL module
The hero_rpc
acl/mod.rsbecomes a consumer of OSIS ACL data rather than the source of truth:Right,Group,Ace,Acl) in hero_rpc for in-memory evaluation (fast path)Aclstruct from stored AclGroups + Context.acl for fast checkingThis way the hero_rpc ACL module doesn't disappear — it becomes the evaluation engine, while OSIS is the persistence layer.
What znzfreezone gets
With this in place,
znzfreezone_backendcan drop its customrpc_auth.rsmiddleware and instead:owner_pubkeyto the admin's pubkey, add AclEntries granting admin/write/read to the appropriate groupsImplementation order
AclRight,AclPrincipal,AclEntry,AclGrouptoschemas/base/acl.oschemaowner_pubkey+aclfields to Context inschemas/base/core.oschemaAclServicewithcheck_access,grant,revoke,resolve_groupsRequestContext(blocked on hero_rpc#11)Add ACL fields to Context type and wire into lifecycle hooksto [Phase 3] Add ACL fields to Context type and wire into lifecycle hooks