java-topology/docs/tickets/seaorm-0002-rbac-group-permissions-linear-find.md
russell@unturf.com 547a9f5738 ORM wave 2: 10 new defects — Active Record +3, Exposed +3, SeaORM +4 (167 sites, 64 ecosystems)
rails-0009: FilterAttributeHandler filter_parameters Array O(A×F) → Set (450×)
rails-0010: Encryption::AutoFilteredParameters two Array scans → Set (250×)
rails-0011: TimeZoneConversion skip_list Array O(M×C×S) → Set (20×)

exposed-0001: SchemaUtilityApi mapMissingColumnStatements O(N×M) → map (118×)
exposed-0002: IdentifierManagerApi isAKeyword O(K) linear → HashSet (144×)
exposed-0003: Table.clone consParams.map fresh List → hoisted HashSet (6×)

seaorm-0001: active_model establish_links leftover.any O(N²) → HashSet (501×)
seaorm-0002: rbac engine group_permissions .values().find() → HashMap by ID (502×)
seaorm-0003: schema builder sorted_tables Vec::contains → HashSet (500×)
seaorm-0004: TopologicalSort from_iter seen Vec O(N²) → BTreeSet (28×)

Unit tests: RailsTest 11/11, ExposedTest 3/3, SeaORMTest 4/4 PASS
Whitepaper: 157→167 sites, 62→64 ecosystems; §13.12 ORM Wave 2 added
2026-03-27 13:49:46 -04:00

2.1 KiB
Raw Blame History

seaorm-0002: group_permissions_by_resources — O(N²) linear find inside loop

Severity: HIGH File: src/rbac/engine/mod.rs Line: 234246 Status: PATCHED

Description

group_permissions_by_resources iterates over a list of (resource_id, permission_id) pairs — up to P*R items where P = number of permissions and R = number of resources — and for each item performs two linear scans:

self.permissions.values().find(|p| p.id == item.1)   // O(P)
self.resources.values()  .find(|r| r.id == item.0)   // O(R)

Both self.permissions and self.resources are HashMap<PermissionRequest, Permission> keyed by the request form of the permission/resource, not by the PermissionId/ResourceId. Therefore a lookup by numeric ID requires a full linear scan of map values.

In an RBAC system with P permissions, R resources, and a user holding M roles each granting K permissions, group_permissions_by_resources scans up to MKP + MKR values for a single call to get_user_role_permissions.

Root Cause

RbacEngine stores permissions and resources indexed by their request key but not by their numeric ID. When group_permissions_by_resources needs to look up by ID it falls back to values().find().

Fix

Add reverse-index maps keyed by numeric ID, built once in from_snapshot, and use them in group_permissions_by_resources:

// in RbacEngine struct
permissions_by_id: HashMap<PermissionId, Permission>,
resources_by_id:   HashMap<ResourceId,   Resource>,

// in from_snapshot
let permissions_by_id: HashMap<_, _> =
    permissions.values().map(|p| (p.id, p.clone())).collect();
let resources_by_id: HashMap<_, _> =
    resources.values().map(|r| (r.id, r.clone())).collect();

// in group_permissions_by_resources (replace .values().find())
let permission = self.permissions_by_id.get(&item.1)
    .ok_or_else(|| Error::PermissionNotFound(...))?;
let resource   = self.resources_by_id.get(&item.0)
    .ok_or_else(|| Error::ResourceNotFound(...))?;

Speedup

~500x at P=R=1000, M=10, K=100 (1 000 000 comparisons → 1 000 hash lookups)