From 7009aa28e7073a57065a175a3ecd3a0e59aaf66d Mon Sep 17 00:00:00 2001 From: Jack Zhuang Date: Sat, 22 Aug 2026 09:29:50 +0800 Subject: [PATCH 1/2] perf(core): batch resolveUserAuthzGrants' independent reads into one wave (#10825) --- ...uthz-context.batch-equivalence.golden.json | 2098 +++++++++++++++++ ...ve-authz-context.batch-equivalence.test.ts | 438 ++++ .../src/security/resolve-authz-context.ts | 72 +- 3 files changed, 2599 insertions(+), 9 deletions(-) create mode 100644 packages/core/src/security/resolve-authz-context.batch-equivalence.golden.json create mode 100644 packages/core/src/security/resolve-authz-context.batch-equivalence.test.ts diff --git a/packages/core/src/security/resolve-authz-context.batch-equivalence.golden.json b/packages/core/src/security/resolve-authz-context.batch-equivalence.golden.json new file mode 100644 index 0000000000..9e1e6b2d70 --- /dev/null +++ b/packages/core/src/security/resolve-authz-context.batch-equivalence.golden.json @@ -0,0 +1,2098 @@ +{ + "empty-principal": { + "sequentialLegs": 5, + "grants": { + "positions": [ + "everyone" + ], + "permissions": [], + "systemPermissions": [], + "org_user_ids": [ + "u_empty" + ], + "accessible_org_ids": [], + "email": "empty@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_empty" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_empty" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_empty" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_empty" + }, + "limit": 1, + "isSystem": true + } + ] + }, + "multi-org-membership": { + "sequentialLegs": 6, + "grants": { + "positions": [ + "org_owner", + "everyone" + ], + "permissions": [], + "systemPermissions": [], + "org_user_ids": [ + "u_multi", + "peer_1" + ], + "accessible_org_ids": [ + "org_a", + "org_b" + ], + "email": "multi@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_multi" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_multi" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_a" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_multi" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_owner", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_multi" + }, + "limit": 1, + "isSystem": true + } + ] + }, + "lapsed-own-membership-among-active-peers": { + "sequentialLegs": 6, + "grants": { + "positions": [ + "org_member", + "everyone" + ], + "permissions": [], + "systemPermissions": [], + "org_user_ids": [ + "u_lapsed", + "peer_1", + "peer_2" + ], + "accessible_org_ids": [], + "email": "lapsed@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_lapsed" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_lapsed" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_a" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_lapsed" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_member", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_lapsed" + }, + "limit": 1, + "isSystem": true + } + ] + }, + "position-derived-grants": { + "sequentialLegs": 8, + "grants": { + "positions": [ + "org_member", + "contributor", + "auditor", + "everyone" + ], + "permissions": [ + "write_all", + "read_all", + "base_access" + ], + "systemPermissions": [ + "record_write" + ], + "org_user_ids": [ + "u_pos" + ], + "accessible_org_ids": [ + "org_a" + ], + "email": "pos@x.com", + "tabPermissions": { + "crm": "visible" + }, + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_pos" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_pos" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_a" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_pos" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_member", + "contributor", + "auditor", + "retired", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position_permission_set", + "where": { + "position_id": { + "$in": [ + "p_contrib", + "p_auditor", + "p_everyone" + ] + } + }, + "limit": 500, + "isSystem": true + }, + { + "object": "sys_permission_set", + "where": { + "id": { + "$in": [ + "ps_write", + "ps_read", + "ps_base" + ] + } + }, + "limit": 500, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_pos" + }, + "limit": 1, + "isSystem": true + } + ] + }, + "permission-set-derived-grants": { + "sequentialLegs": 7, + "grants": { + "positions": [ + "platform_admin", + "org_member", + "everyone" + ], + "permissions": [ + "admin_full_access", + "org_tools" + ], + "systemPermissions": [ + "manage_users" + ], + "org_user_ids": [ + "u_ps" + ], + "accessible_org_ids": [ + "org_a" + ], + "email": "ps@x.com", + "tabPermissions": { + "crm": "hidden" + }, + "posture": "PLATFORM_ADMIN" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_ps" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_ps" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_a" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_ps" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "platform_admin", + "org_member", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_permission_set", + "where": { + "id": { + "$in": [ + "ps_admin", + "ps_org", + "ps_dead" + ] + } + }, + "limit": 500, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_ps" + }, + "limit": 1, + "isSystem": true + } + ] + }, + "tenant-admin-via-position": { + "sequentialLegs": 8, + "grants": { + "positions": [ + "org_admin", + "everyone" + ], + "permissions": [ + "low", + "organization_admin" + ], + "systemPermissions": [], + "org_user_ids": [ + "u_ta" + ], + "accessible_org_ids": [ + "org_a" + ], + "email": "ta@x.com", + "tabPermissions": { + "crm": "default_on" + }, + "posture": "TENANT_ADMIN" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_ta" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_ta" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_a" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_ta" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_admin", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position_permission_set", + "where": { + "position_id": { + "$in": [ + "p_orgadmin", + "p_everyone" + ] + } + }, + "limit": 500, + "isSystem": true + }, + { + "object": "sys_permission_set", + "where": { + "id": { + "$in": [ + "ps_low", + "ps_oa" + ] + } + }, + "limit": 500, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_ta" + }, + "limit": 1, + "isSystem": true + } + ] + }, + "ai-seat-and-email-from-sys-user": { + "sequentialLegs": 6, + "grants": { + "positions": [ + "org_member", + "everyone" + ], + "permissions": [ + "ai_seat" + ], + "systemPermissions": [], + "org_user_ids": [ + "u_ai" + ], + "accessible_org_ids": [ + "org_a" + ], + "email": "ai@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_user", + "where": { + "id": "u_ai" + }, + "limit": 1, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "user_id": "u_ai" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_ai" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_a" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_ai" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_member", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + } + ] + }, + "ai-seat-denied": { + "sequentialLegs": 5, + "grants": { + "positions": [ + "everyone" + ], + "permissions": [], + "systemPermissions": [], + "org_user_ids": [ + "u_noai" + ], + "accessible_org_ids": [], + "email": "noai@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_user", + "where": { + "id": "u_noai" + }, + "limit": 1, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "user_id": "u_noai" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_noai" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_noai" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + } + ] + }, + "seeded-permissions-and-email": { + "sequentialLegs": 6, + "grants": { + "positions": [ + "org_member", + "everyone" + ], + "permissions": [ + "key_scope_b", + "key_scope_a", + "ai_seat", + "extra" + ], + "systemPermissions": [], + "org_user_ids": [ + "u_seed" + ], + "accessible_org_ids": [ + "org_a" + ], + "email": "seed@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_seed" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_seed" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_a" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_seed" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_member", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_permission_set", + "where": { + "id": { + "$in": [ + "ps_x" + ] + } + }, + "limit": 500, + "isSystem": true + } + ] + }, + "read-limits-truncate": { + "sequentialLegs": 6, + "grants": { + "positions": [ + "org_member", + "everyone" + ], + "permissions": [], + "systemPermissions": [], + "org_user_ids": [ + "u_lim", + "peer_0000", + "peer_0001", + "peer_0002", + "peer_0003", + "peer_0004", + "peer_0005", + "peer_0006", + "peer_0007", + "peer_0008", + "peer_0009", + "peer_0010", + "peer_0011", + "peer_0012", + "peer_0013", + "peer_0014", + "peer_0015", + "peer_0016", + "peer_0017", + "peer_0018", + "peer_0019", + "peer_0020", + "peer_0021", + "peer_0022", + "peer_0023", + "peer_0024", + "peer_0025", + "peer_0026", + "peer_0027", + "peer_0028", + "peer_0029", + "peer_0030", + "peer_0031", + "peer_0032", + "peer_0033", + "peer_0034", + "peer_0035", + "peer_0036", + "peer_0037", + "peer_0038", + "peer_0039", + "peer_0040", + "peer_0041", + "peer_0042", + "peer_0043", + "peer_0044", + "peer_0045", + "peer_0046", + "peer_0047", + "peer_0048", + "peer_0049", + "peer_0050", + "peer_0051", + "peer_0052", + "peer_0053", + "peer_0054", + "peer_0055", + "peer_0056", + "peer_0057", + "peer_0058", + "peer_0059", + "peer_0060", + "peer_0061", + "peer_0062", + "peer_0063", + "peer_0064", + "peer_0065", + "peer_0066", + "peer_0067", + "peer_0068", + "peer_0069", + "peer_0070", + "peer_0071", + "peer_0072", + "peer_0073", + "peer_0074", + "peer_0075", + "peer_0076", + "peer_0077", + "peer_0078", + "peer_0079", + "peer_0080", + "peer_0081", + "peer_0082", + "peer_0083", + "peer_0084", + "peer_0085", + "peer_0086", + "peer_0087", + "peer_0088", + "peer_0089", + "peer_0090", + "peer_0091", + "peer_0092", + "peer_0093", + "peer_0094", + "peer_0095", + "peer_0096", + "peer_0097", + "peer_0098", + "peer_0099", + "peer_0100", + "peer_0101", + "peer_0102", + "peer_0103", + "peer_0104", + "peer_0105", + "peer_0106", + "peer_0107", + "peer_0108", + "peer_0109", + "peer_0110", + "peer_0111", + "peer_0112", + "peer_0113", + "peer_0114", + "peer_0115", + "peer_0116", + "peer_0117", + "peer_0118", + "peer_0119", + "peer_0120", + "peer_0121", + "peer_0122", + "peer_0123", + "peer_0124", + "peer_0125", + "peer_0126", + "peer_0127", + "peer_0128", + "peer_0129", + "peer_0130", + "peer_0131", + "peer_0132", + "peer_0133", + "peer_0134", + "peer_0135", + "peer_0136", + "peer_0137", + "peer_0138", + "peer_0139", + "peer_0140", + "peer_0141", + "peer_0142", + "peer_0143", + "peer_0144", + "peer_0145", + "peer_0146", + "peer_0147", + "peer_0148", + "peer_0149", + "peer_0150", + "peer_0151", + "peer_0152", + "peer_0153", + "peer_0154", + "peer_0155", + "peer_0156", + "peer_0157", + "peer_0158", + "peer_0159", + "peer_0160", + "peer_0161", + "peer_0162", + "peer_0163", + "peer_0164", + "peer_0165", + "peer_0166", + "peer_0167", + "peer_0168", + "peer_0169", + "peer_0170", + "peer_0171", + "peer_0172", + "peer_0173", + "peer_0174", + "peer_0175", + "peer_0176", + "peer_0177", + "peer_0178", + "peer_0179", + "peer_0180", + "peer_0181", + "peer_0182", + "peer_0183", + "peer_0184", + "peer_0185", + "peer_0186", + "peer_0187", + "peer_0188", + "peer_0189", + "peer_0190", + "peer_0191", + "peer_0192", + "peer_0193", + "peer_0194", + "peer_0195", + "peer_0196", + "peer_0197", + "peer_0198", + "peer_0199", + "peer_0200", + "peer_0201", + "peer_0202", + "peer_0203", + "peer_0204", + "peer_0205", + "peer_0206", + "peer_0207", + "peer_0208", + "peer_0209", + "peer_0210", + "peer_0211", + "peer_0212", + "peer_0213", + "peer_0214", + "peer_0215", + "peer_0216", + "peer_0217", + "peer_0218", + "peer_0219", + "peer_0220", + "peer_0221", + "peer_0222", + "peer_0223", + "peer_0224", + "peer_0225", + "peer_0226", + "peer_0227", + "peer_0228", + "peer_0229", + "peer_0230", + "peer_0231", + "peer_0232", + "peer_0233", + "peer_0234", + "peer_0235", + "peer_0236", + "peer_0237", + "peer_0238", + "peer_0239", + "peer_0240", + "peer_0241", + "peer_0242", + "peer_0243", + "peer_0244", + "peer_0245", + "peer_0246", + "peer_0247", + "peer_0248", + "peer_0249", + "peer_0250", + "peer_0251", + "peer_0252", + "peer_0253", + "peer_0254", + "peer_0255", + "peer_0256", + "peer_0257", + "peer_0258", + "peer_0259", + "peer_0260", + "peer_0261", + "peer_0262", + "peer_0263", + "peer_0264", + "peer_0265", + "peer_0266", + "peer_0267", + "peer_0268", + "peer_0269", + "peer_0270", + "peer_0271", + "peer_0272", + "peer_0273", + "peer_0274", + "peer_0275", + "peer_0276", + "peer_0277", + "peer_0278", + "peer_0279", + "peer_0280", + "peer_0281", + "peer_0282", + "peer_0283", + "peer_0284", + "peer_0285", + "peer_0286", + "peer_0287", + "peer_0288", + "peer_0289", + "peer_0290", + "peer_0291", + "peer_0292", + "peer_0293", + "peer_0294", + "peer_0295", + "peer_0296", + "peer_0297", + "peer_0298", + "peer_0299", + "peer_0300", + "peer_0301", + "peer_0302", + "peer_0303", + "peer_0304", + "peer_0305", + "peer_0306", + "peer_0307", + "peer_0308", + "peer_0309", + "peer_0310", + "peer_0311", + "peer_0312", + "peer_0313", + "peer_0314", + "peer_0315", + "peer_0316", + "peer_0317", + "peer_0318", + "peer_0319", + "peer_0320", + "peer_0321", + "peer_0322", + "peer_0323", + "peer_0324", + "peer_0325", + "peer_0326", + "peer_0327", + "peer_0328", + "peer_0329", + "peer_0330", + "peer_0331", + "peer_0332", + "peer_0333", + "peer_0334", + "peer_0335", + "peer_0336", + "peer_0337", + "peer_0338", + "peer_0339", + "peer_0340", + "peer_0341", + "peer_0342", + "peer_0343", + "peer_0344", + "peer_0345", + "peer_0346", + "peer_0347", + "peer_0348", + "peer_0349", + "peer_0350", + "peer_0351", + "peer_0352", + "peer_0353", + "peer_0354", + "peer_0355", + "peer_0356", + "peer_0357", + "peer_0358", + "peer_0359", + "peer_0360", + "peer_0361", + "peer_0362", + "peer_0363", + "peer_0364", + "peer_0365", + "peer_0366", + "peer_0367", + "peer_0368", + "peer_0369", + "peer_0370", + "peer_0371", + "peer_0372", + "peer_0373", + "peer_0374", + "peer_0375", + "peer_0376", + "peer_0377", + "peer_0378", + "peer_0379", + "peer_0380", + "peer_0381", + "peer_0382", + "peer_0383", + "peer_0384", + "peer_0385", + "peer_0386", + "peer_0387", + "peer_0388", + "peer_0389", + "peer_0390", + "peer_0391", + "peer_0392", + "peer_0393", + "peer_0394", + "peer_0395", + "peer_0396", + "peer_0397", + "peer_0398", + "peer_0399", + "peer_0400", + "peer_0401", + "peer_0402", + "peer_0403", + "peer_0404", + "peer_0405", + "peer_0406", + "peer_0407", + "peer_0408", + "peer_0409", + "peer_0410", + "peer_0411", + "peer_0412", + "peer_0413", + "peer_0414", + "peer_0415", + "peer_0416", + "peer_0417", + "peer_0418", + "peer_0419", + "peer_0420", + "peer_0421", + "peer_0422", + "peer_0423", + "peer_0424", + "peer_0425", + "peer_0426", + "peer_0427", + "peer_0428", + "peer_0429", + "peer_0430", + "peer_0431", + "peer_0432", + "peer_0433", + "peer_0434", + "peer_0435", + "peer_0436", + "peer_0437", + "peer_0438", + "peer_0439", + "peer_0440", + "peer_0441", + "peer_0442", + "peer_0443", + "peer_0444", + "peer_0445", + "peer_0446", + "peer_0447", + "peer_0448", + "peer_0449", + "peer_0450", + "peer_0451", + "peer_0452", + "peer_0453", + "peer_0454", + "peer_0455", + "peer_0456", + "peer_0457", + "peer_0458", + "peer_0459", + "peer_0460", + "peer_0461", + "peer_0462", + "peer_0463", + "peer_0464", + "peer_0465", + "peer_0466", + "peer_0467", + "peer_0468", + "peer_0469", + "peer_0470", + "peer_0471", + "peer_0472", + "peer_0473", + "peer_0474", + "peer_0475", + "peer_0476", + "peer_0477", + "peer_0478", + "peer_0479", + "peer_0480", + "peer_0481", + "peer_0482", + "peer_0483", + "peer_0484", + "peer_0485", + "peer_0486", + "peer_0487", + "peer_0488", + "peer_0489", + "peer_0490", + "peer_0491", + "peer_0492", + "peer_0493", + "peer_0494", + "peer_0495", + "peer_0496", + "peer_0497", + "peer_0498", + "peer_0499", + "peer_0500", + "peer_0501", + "peer_0502", + "peer_0503", + "peer_0504", + "peer_0505", + "peer_0506", + "peer_0507", + "peer_0508", + "peer_0509", + "peer_0510", + "peer_0511", + "peer_0512", + "peer_0513", + "peer_0514", + "peer_0515", + "peer_0516", + "peer_0517", + "peer_0518", + "peer_0519", + "peer_0520", + "peer_0521", + "peer_0522", + "peer_0523", + "peer_0524", + "peer_0525", + "peer_0526", + "peer_0527", + "peer_0528", + "peer_0529", + "peer_0530", + "peer_0531", + "peer_0532", + "peer_0533", + "peer_0534", + "peer_0535", + "peer_0536", + "peer_0537", + "peer_0538", + "peer_0539", + "peer_0540", + "peer_0541", + "peer_0542", + "peer_0543", + "peer_0544", + "peer_0545", + "peer_0546", + "peer_0547", + "peer_0548", + "peer_0549", + "peer_0550", + "peer_0551", + "peer_0552", + "peer_0553", + "peer_0554", + "peer_0555", + "peer_0556", + "peer_0557", + "peer_0558", + "peer_0559", + "peer_0560", + "peer_0561", + "peer_0562", + "peer_0563", + "peer_0564", + "peer_0565", + "peer_0566", + "peer_0567", + "peer_0568", + "peer_0569", + "peer_0570", + "peer_0571", + "peer_0572", + "peer_0573", + "peer_0574", + "peer_0575", + "peer_0576", + "peer_0577", + "peer_0578", + "peer_0579", + "peer_0580", + "peer_0581", + "peer_0582", + "peer_0583", + "peer_0584", + "peer_0585", + "peer_0586", + "peer_0587", + "peer_0588", + "peer_0589", + "peer_0590", + "peer_0591", + "peer_0592", + "peer_0593", + "peer_0594", + "peer_0595", + "peer_0596", + "peer_0597", + "peer_0598", + "peer_0599", + "peer_0600", + "peer_0601", + "peer_0602", + "peer_0603", + "peer_0604", + "peer_0605", + "peer_0606", + "peer_0607", + "peer_0608", + "peer_0609", + "peer_0610", + "peer_0611", + "peer_0612", + "peer_0613", + "peer_0614", + "peer_0615", + "peer_0616", + "peer_0617", + "peer_0618", + "peer_0619", + "peer_0620", + "peer_0621", + "peer_0622", + "peer_0623", + "peer_0624", + "peer_0625", + "peer_0626", + "peer_0627", + "peer_0628", + "peer_0629", + "peer_0630", + "peer_0631", + "peer_0632", + "peer_0633", + "peer_0634", + "peer_0635", + "peer_0636", + "peer_0637", + "peer_0638", + "peer_0639", + "peer_0640", + "peer_0641", + "peer_0642", + "peer_0643", + "peer_0644", + "peer_0645", + "peer_0646", + "peer_0647", + "peer_0648", + "peer_0649", + "peer_0650", + "peer_0651", + "peer_0652", + "peer_0653", + "peer_0654", + "peer_0655", + "peer_0656", + "peer_0657", + "peer_0658", + "peer_0659", + "peer_0660", + "peer_0661", + "peer_0662", + "peer_0663", + "peer_0664", + "peer_0665", + "peer_0666", + "peer_0667", + "peer_0668", + "peer_0669", + "peer_0670", + "peer_0671", + "peer_0672", + "peer_0673", + "peer_0674", + "peer_0675", + "peer_0676", + "peer_0677", + "peer_0678", + "peer_0679", + "peer_0680", + "peer_0681", + "peer_0682", + "peer_0683", + "peer_0684", + "peer_0685", + "peer_0686", + "peer_0687", + "peer_0688", + "peer_0689", + "peer_0690", + "peer_0691", + "peer_0692", + "peer_0693", + "peer_0694", + "peer_0695", + "peer_0696", + "peer_0697", + "peer_0698", + "peer_0699", + "peer_0700", + "peer_0701", + "peer_0702", + "peer_0703", + "peer_0704", + "peer_0705", + "peer_0706", + "peer_0707", + "peer_0708", + "peer_0709", + "peer_0710", + "peer_0711", + "peer_0712", + "peer_0713", + "peer_0714", + "peer_0715", + "peer_0716", + "peer_0717", + "peer_0718", + "peer_0719", + "peer_0720", + "peer_0721", + "peer_0722", + "peer_0723", + "peer_0724", + "peer_0725", + "peer_0726", + "peer_0727", + "peer_0728", + "peer_0729", + "peer_0730", + "peer_0731", + "peer_0732", + "peer_0733", + "peer_0734", + "peer_0735", + "peer_0736", + "peer_0737", + "peer_0738", + "peer_0739", + "peer_0740", + "peer_0741", + "peer_0742", + "peer_0743", + "peer_0744", + "peer_0745", + "peer_0746", + "peer_0747", + "peer_0748", + "peer_0749", + "peer_0750", + "peer_0751", + "peer_0752", + "peer_0753", + "peer_0754", + "peer_0755", + "peer_0756", + "peer_0757", + "peer_0758", + "peer_0759", + "peer_0760", + "peer_0761", + "peer_0762", + "peer_0763", + "peer_0764", + "peer_0765", + "peer_0766", + "peer_0767", + "peer_0768", + "peer_0769", + "peer_0770", + "peer_0771", + "peer_0772", + "peer_0773", + "peer_0774", + "peer_0775", + "peer_0776", + "peer_0777", + "peer_0778", + "peer_0779", + "peer_0780", + "peer_0781", + "peer_0782", + "peer_0783", + "peer_0784", + "peer_0785", + "peer_0786", + "peer_0787", + "peer_0788", + "peer_0789", + "peer_0790", + "peer_0791", + "peer_0792", + "peer_0793", + "peer_0794", + "peer_0795", + "peer_0796", + "peer_0797", + "peer_0798", + "peer_0799", + "peer_0800", + "peer_0801", + "peer_0802", + "peer_0803", + "peer_0804", + "peer_0805", + "peer_0806", + "peer_0807", + "peer_0808", + "peer_0809", + "peer_0810", + "peer_0811", + "peer_0812", + "peer_0813", + "peer_0814", + "peer_0815", + "peer_0816", + "peer_0817", + "peer_0818", + "peer_0819", + "peer_0820", + "peer_0821", + "peer_0822", + "peer_0823", + "peer_0824", + "peer_0825", + "peer_0826", + "peer_0827", + "peer_0828", + "peer_0829", + "peer_0830", + "peer_0831", + "peer_0832", + "peer_0833", + "peer_0834", + "peer_0835", + "peer_0836", + "peer_0837", + "peer_0838", + "peer_0839", + "peer_0840", + "peer_0841", + "peer_0842", + "peer_0843", + "peer_0844", + "peer_0845", + "peer_0846", + "peer_0847", + "peer_0848", + "peer_0849", + "peer_0850", + "peer_0851", + "peer_0852", + "peer_0853", + "peer_0854", + "peer_0855", + "peer_0856", + "peer_0857", + "peer_0858", + "peer_0859", + "peer_0860", + "peer_0861", + "peer_0862", + "peer_0863", + "peer_0864", + "peer_0865", + "peer_0866", + "peer_0867", + "peer_0868", + "peer_0869", + "peer_0870", + "peer_0871", + "peer_0872", + "peer_0873", + "peer_0874", + "peer_0875", + "peer_0876", + "peer_0877", + "peer_0878", + "peer_0879", + "peer_0880", + "peer_0881", + "peer_0882", + "peer_0883", + "peer_0884", + "peer_0885", + "peer_0886", + "peer_0887", + "peer_0888", + "peer_0889", + "peer_0890", + "peer_0891", + "peer_0892", + "peer_0893", + "peer_0894", + "peer_0895", + "peer_0896", + "peer_0897", + "peer_0898", + "peer_0899", + "peer_0900", + "peer_0901", + "peer_0902", + "peer_0903", + "peer_0904", + "peer_0905", + "peer_0906", + "peer_0907", + "peer_0908", + "peer_0909", + "peer_0910", + "peer_0911", + "peer_0912", + "peer_0913", + "peer_0914", + "peer_0915", + "peer_0916", + "peer_0917", + "peer_0918", + "peer_0919", + "peer_0920", + "peer_0921", + "peer_0922", + "peer_0923", + "peer_0924", + "peer_0925", + "peer_0926", + "peer_0927", + "peer_0928", + "peer_0929", + "peer_0930", + "peer_0931", + "peer_0932", + "peer_0933", + "peer_0934", + "peer_0935", + "peer_0936", + "peer_0937", + "peer_0938", + "peer_0939", + "peer_0940", + "peer_0941", + "peer_0942", + "peer_0943", + "peer_0944", + "peer_0945", + "peer_0946", + "peer_0947", + "peer_0948", + "peer_0949", + "peer_0950", + "peer_0951", + "peer_0952", + "peer_0953", + "peer_0954", + "peer_0955", + "peer_0956", + "peer_0957", + "peer_0958", + "peer_0959", + "peer_0960", + "peer_0961", + "peer_0962", + "peer_0963", + "peer_0964", + "peer_0965", + "peer_0966", + "peer_0967", + "peer_0968", + "peer_0969", + "peer_0970", + "peer_0971", + "peer_0972", + "peer_0973", + "peer_0974", + "peer_0975", + "peer_0976", + "peer_0977", + "peer_0978", + "peer_0979", + "peer_0980", + "peer_0981", + "peer_0982", + "peer_0983", + "peer_0984", + "peer_0985", + "peer_0986", + "peer_0987", + "peer_0988", + "peer_0989", + "peer_0990", + "peer_0991", + "peer_0992", + "peer_0993", + "peer_0994", + "peer_0995", + "peer_0996", + "peer_0997", + "peer_0998" + ], + "accessible_org_ids": [ + "org_0000", + "org_0001", + "org_0002", + "org_0003", + "org_0004", + "org_0005", + "org_0006", + "org_0007", + "org_0008", + "org_0009", + "org_0010", + "org_0011", + "org_0012", + "org_0013", + "org_0014", + "org_0015", + "org_0016", + "org_0017", + "org_0018", + "org_0019", + "org_0020", + "org_0021", + "org_0022", + "org_0023", + "org_0024", + "org_0025", + "org_0026", + "org_0027", + "org_0028", + "org_0029", + "org_0030", + "org_0031", + "org_0032", + "org_0033", + "org_0034", + "org_0035", + "org_0036", + "org_0037", + "org_0038", + "org_0039", + "org_0040", + "org_0041", + "org_0042", + "org_0043", + "org_0044", + "org_0045", + "org_0046", + "org_0047", + "org_0048", + "org_0049", + "org_0050", + "org_0051", + "org_0052", + "org_0053", + "org_0054", + "org_0055", + "org_0056", + "org_0057", + "org_0058", + "org_0059", + "org_0060", + "org_0061", + "org_0062", + "org_0063", + "org_0064", + "org_0065", + "org_0066", + "org_0067", + "org_0068", + "org_0069", + "org_0070", + "org_0071", + "org_0072", + "org_0073", + "org_0074", + "org_0075", + "org_0076", + "org_0077", + "org_0078", + "org_0079", + "org_0080", + "org_0081", + "org_0082", + "org_0083", + "org_0084", + "org_0085", + "org_0086", + "org_0087", + "org_0088", + "org_0089", + "org_0090", + "org_0091", + "org_0092", + "org_0093", + "org_0094", + "org_0095", + "org_0096", + "org_0097", + "org_0098", + "org_0099", + "org_0100", + "org_0101", + "org_0102", + "org_0103", + "org_0104", + "org_0105", + "org_0106", + "org_0107", + "org_0108", + "org_0109", + "org_0110", + "org_0111", + "org_0112", + "org_0113", + "org_0114", + "org_0115", + "org_0116", + "org_0117", + "org_0118", + "org_0119", + "org_0120", + "org_0121", + "org_0122", + "org_0123", + "org_0124", + "org_0125", + "org_0126", + "org_0127", + "org_0128", + "org_0129", + "org_0130", + "org_0131", + "org_0132", + "org_0133", + "org_0134", + "org_0135", + "org_0136", + "org_0137", + "org_0138", + "org_0139", + "org_0140", + "org_0141", + "org_0142", + "org_0143", + "org_0144", + "org_0145", + "org_0146", + "org_0147", + "org_0148", + "org_0149", + "org_0150", + "org_0151", + "org_0152", + "org_0153", + "org_0154", + "org_0155", + "org_0156", + "org_0157", + "org_0158", + "org_0159", + "org_0160", + "org_0161", + "org_0162", + "org_0163", + "org_0164", + "org_0165", + "org_0166", + "org_0167", + "org_0168", + "org_0169", + "org_0170", + "org_0171", + "org_0172", + "org_0173", + "org_0174", + "org_0175", + "org_0176", + "org_0177", + "org_0178", + "org_0179", + "org_0180", + "org_0181", + "org_0182", + "org_0183", + "org_0184", + "org_0185", + "org_0186", + "org_0187", + "org_0188", + "org_0189", + "org_0190", + "org_0191", + "org_0192", + "org_0193", + "org_0194", + "org_0195", + "org_0196", + "org_0197", + "org_0198", + "org_0199" + ], + "email": "lim@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_lim" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_lim" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_member", + "where": { + "organization_id": "org_0000" + }, + "limit": 1000, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_lim" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_member", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_lim" + }, + "limit": 1, + "isSystem": true + } + ] + }, + "no-active-org": { + "sequentialLegs": 5, + "grants": { + "positions": [ + "org_owner", + "org_member", + "org_admin", + "everyone" + ], + "permissions": [], + "systemPermissions": [], + "org_user_ids": [ + "u_noorg" + ], + "accessible_org_ids": [ + "org_a", + "org_b" + ], + "email": "noorg@x.com", + "posture": "MEMBER" + }, + "queries": [ + { + "object": "sys_member", + "where": { + "user_id": "u_noorg" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_position", + "where": { + "user_id": "u_noorg" + }, + "limit": 200, + "isSystem": true + }, + { + "object": "sys_user_permission_set", + "where": { + "user_id": "u_noorg" + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_position", + "where": { + "name": { + "$in": [ + "org_owner", + "org_member", + "org_admin", + "everyone" + ] + } + }, + "limit": 100, + "isSystem": true + }, + { + "object": "sys_user", + "where": { + "id": "u_noorg" + }, + "limit": 1, + "isSystem": true + } + ] + } +} diff --git a/packages/core/src/security/resolve-authz-context.batch-equivalence.test.ts b/packages/core/src/security/resolve-authz-context.batch-equivalence.test.ts new file mode 100644 index 0000000000..56d24cd792 --- /dev/null +++ b/packages/core/src/security/resolve-authz-context.batch-equivalence.test.ts @@ -0,0 +1,438 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#10825] Row-equivalence proof for the BATCHED `resolveUserAuthzGrants`. + * + * `resolveUserAuthzGrants` used to issue its eight reads one after another — + * legs 6–13 of an authenticated request. They are now issued in four waves + * (`Promise.all` over the reads that have no data dependency on one another). + * Latency-wise that is the whole point (cloud#1539: sequential LEGS, not query + * count, are the multiplier), but on an AUTHORIZATION path a re-ordering that + * changes even one returned row is a privilege bug that a "the request still + * succeeds" suite cannot see. + * + * So this file is a DIFFERENTIAL CONTROL, not a smoke test. Every expectation + * below was CAPTURED from the pre-fix sequential implementation + * (`git show 38bc74ed1:packages/core/src/security/resolve-authz-context.ts`) + * running against these exact fixtures, and is asserted verbatim against the + * batched one. Two independent goldens per fixture: + * + * 1. `grants` — the whole resolved envelope, deep-equal INCLUDING array + * order (positions/permissions order is contractual: `seedPermissions` + * first, `platform_admin` unshifted to the front, …). + * 2. `queries` — the exact multiset of `{ object, where, limit }` triples the + * resolver issued, in issue order, each with `context.isSystem === true`. + * This is the "same filters, same tenancy scoping, same limits" half: + * widening an `$in`, dropping a tenancy filter or merging two reads into + * one changes this list even when the resolved envelope happens to agree. + * + * A third, non-golden assertion measures the LEG count (see `makeRecordingQl`) + * — that one is the changed-behaviour control: it read 8 pre-fix and reads 4 + * now. + * + * ⛔ These goldens are a record of what the sequential code DID. Never "fix" a + * red one by re-capturing it: the whole value of the file is that it cannot be + * satisfied by agreeing with the new implementation. + */ + +import { readFileSync } from 'node:fs'; +import { describe, it, expect } from 'vitest'; +import { resolveUserAuthzGrants } from './resolve-authz-context.js'; +import type { ResolveUserAuthzGrantsOptions } from './resolve-authz-context.js'; + +// ── Recording ObjectQL double ─────────────────────────────────────────────── + +interface RecordedCall { object: string; where: unknown; limit: unknown; isSystem: boolean } + +/** + * An in-memory ObjectQL double that (a) records every read, (b) ENFORCES the + * `limit` the caller passed — a real driver does, and a batch that quietly + * changed a limit would otherwise be invisible — and (c) counts LEGS. + * + * Leg counting: every read yields on a real macrotask boundary before it + * answers, so reads issued together are genuinely in flight together. A read + * that starts while nothing else is in flight OPENS a leg; one that starts + * while another is in flight JOINS the open leg. Sequential awaits therefore + * count one leg each, and a `Promise.all` of any width counts one — which is + * exactly the definition cloud#1539 measured latency against. + */ +function makeRecordingQl(tables: Record) { + const calls: RecordedCall[] = []; + const legOf: number[] = []; + let inFlight = 0; + let legs = 0; + const matches = (row: any, where: any): boolean => + Object.entries(where ?? {}).every(([k, v]) => { + if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); + if (v && typeof v === 'object' && '$in' in (v as any)) return (v as any).$in.includes(row[k]); + return row[k] === v; + }); + return { + calls, + legOf, + get legs() { return legs; }, + async find(object: string, opts: any) { + if (inFlight === 0) legs += 1; + inFlight += 1; + legOf.push(legs); + calls.push({ + object, + where: opts?.where, + limit: opts?.limit, + isSystem: opts?.context?.isSystem === true, + }); + try { + await new Promise((r) => setTimeout(r, 0)); + const rows = (tables[object] ?? []).filter((r) => matches(r, opts?.where)); + return typeof opts?.limit === 'number' ? rows.slice(0, opts.limit) : rows; + } finally { + inFlight -= 1; + } + }, + }; +} + +// ── Fixture matrix — one entry per shape the eight reads discriminate on ──── + +const T0 = Date.UTC(2026, 0, 1); // fixed clock; every validity window is relative to it +const past = new Date(T0 - 86_400_000).toISOString(); +const future = new Date(T0 + 86_400_000).toISOString(); + +interface Fixture { + name: string; + userId: string; + opts: ResolveUserAuthzGrantsOptions; + tables: Record; +} + +/** 205 memberships for one user — the `sys_member {user_id}` limit is 200. */ +const manyOwnMemberships = Array.from({ length: 205 }, (_, i) => ({ + user_id: 'u_lim', + organization_id: `org_${String(i).padStart(4, '0')}`, + role: 'member', +})); +/** 1005 peers in the active org — the fellow-org `sys_member` limit is 1000. */ +const manyPeers = Array.from({ length: 1005 }, (_, i) => ({ + user_id: `peer_${String(i).padStart(4, '0')}`, + organization_id: 'org_0000', + role: 'member', +})); + +const FIXTURES: Fixture[] = [ + { + // An authenticated principal that holds nothing at all. Fails closed to the + // `everyone` anchor and an empty everything-else — never null. + name: 'empty-principal', + userId: 'u_empty', + opts: { seedEmail: 'empty@x.com', nowMs: T0 }, + tables: { sys_user: [{ id: 'u_empty' }], sys_member: [], sys_user_position: [], sys_user_permission_set: [] }, + }, + { + // Multi-org membership: positions come from the ACTIVE org only, while + // `accessible_org_ids` spans every org — the two facts the ONE sys_member + // read must keep in agreement. + name: 'multi-org-membership', + userId: 'u_multi', + opts: { tenantId: 'org_a', seedEmail: 'multi@x.com', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_multi' }], + sys_member: [ + { user_id: 'u_multi', organization_id: 'org_a', role: 'owner' }, + { user_id: 'u_multi', organization_id: 'org_b', role: 'admin' }, + { user_id: 'u_multi', organization_id: 'org_c', role: 'member', valid_until: past }, + { user_id: 'peer_1', organization_id: 'org_a', role: 'owner' }, + { user_id: 'peer_2', organization_id: 'org_b', role: 'owner' }, + ], + sys_user_position: [], + sys_user_permission_set: [], + }, + }, + { + // ⚠️ THE DIVERGENCE CASE the batch could plausibly introduce. + // + // `sys_member {user_id}` (memberships + org-admin roles) and + // `sys_member {organization_id}` (fellow-org peers, limit 1000) read the + // SAME table and are now issued in the same wave — the obvious "improvement" + // is to merge them into one `$or`/unfiltered read and partition in memory. + // Here that merge is a privilege escalation with no error anywhere: + // `u_lapsed`'s OWN membership in org_a has lapsed (ADR-0091), while peers + // hold ACTIVE `owner` rows in org_a. A merged read would put those peer rows + // through the `accessible_org_ids` loop (granting org_a — the `group` + // posture's whole read reach) and through the `activeMembers` role loop + // (granting `org_owner`, and with it TENANT_ADMIN). + // + // The golden below pins the sequential answer: `accessible_org_ids: []`, + // no `org_owner`, posture MEMBER. The peer rows still appear in + // `org_user_ids`, because THAT is what the fellow-org read is for. + name: 'lapsed-own-membership-among-active-peers', + userId: 'u_lapsed', + opts: { tenantId: 'org_a', seedEmail: 'lapsed@x.com', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_lapsed' }], + sys_member: [ + { user_id: 'u_lapsed', organization_id: 'org_a', role: 'member', valid_until: past }, + { user_id: 'peer_1', organization_id: 'org_a', role: 'owner' }, + { user_id: 'peer_2', organization_id: 'org_a', role: 'admin' }, + ], + sys_user_position: [], + sys_user_permission_set: [], + }, + }, + { + // Position-derived grants: a global row, an org-scoped row for ANOTHER org + // (must not resolve), a lapsed row, a not-yet-valid row, plus the ADR-0049 + // deactivated position whose NAME must leave `positions` too. + name: 'position-derived-grants', + userId: 'u_pos', + opts: { tenantId: 'org_a', seedEmail: 'pos@x.com', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_pos' }], + sys_member: [{ user_id: 'u_pos', organization_id: 'org_a', role: 'member' }], + sys_user_position: [ + { user_id: 'u_pos', position: 'contributor', organization_id: null }, + { user_id: 'u_pos', position: 'auditor', organization_id: 'org_a' }, + { user_id: 'u_pos', position: 'foreigner', organization_id: 'org_z' }, + { user_id: 'u_pos', position: 'expired_role', organization_id: null, valid_until: past }, + { user_id: 'u_pos', position: 'future_role', organization_id: null, valid_from: future }, + { user_id: 'u_pos', position: 'retired', organization_id: null }, + ], + sys_position: [ + { id: 'p_contrib', name: 'contributor' }, + { id: 'p_auditor', name: 'auditor', active: true }, + { id: 'p_retired', name: 'retired', active: false }, + { id: 'p_everyone', name: 'everyone' }, + ], + sys_position_permission_set: [ + { position_id: 'p_contrib', permission_set_id: 'ps_write' }, + { position_id: 'p_auditor', permission_set_id: 'ps_read' }, + { position_id: 'p_retired', permission_set_id: 'ps_admin' }, + { position_id: 'p_everyone', permission_set_id: 'ps_base' }, + ], + sys_permission_set: [ + { id: 'ps_write', name: 'write_all', system_permissions: '["record_write"]' }, + { id: 'ps_read', name: 'read_all', tab_permissions: '{"crm":"visible"}' }, + { id: 'ps_admin', name: 'admin_full_access' }, + { id: 'ps_base', name: 'base_access', tab_permissions: { crm: 'default_on' } }, + ], + sys_user_permission_set: [], + }, + }, + { + // Permission-set-derived grants: the UNSCOPED `admin_full_access` user grant + // is the ONLY thing that derives platform_admin, an org-scoped copy is not, + // a lapsed one is not, and a DEACTIVATED set grants nothing at all. + name: 'permission-set-derived-grants', + userId: 'u_ps', + opts: { tenantId: 'org_a', seedEmail: 'ps@x.com', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_ps' }], + sys_member: [{ user_id: 'u_ps', organization_id: 'org_a', role: 'member' }], + sys_user_position: [], + sys_user_permission_set: [ + { user_id: 'u_ps', permission_set_id: 'ps_admin', organization_id: null }, + { user_id: 'u_ps', permission_set_id: 'ps_org', organization_id: 'org_a' }, + { user_id: 'u_ps', permission_set_id: 'ps_other', organization_id: 'org_z' }, + { user_id: 'u_ps', permission_set_id: 'ps_lapsed', organization_id: null, valid_until: past }, + { user_id: 'u_ps', permission_set_id: 'ps_dead', organization_id: null }, + ], + sys_permission_set: [ + { id: 'ps_admin', name: 'admin_full_access', system_permissions: ['manage_users'] }, + { id: 'ps_org', name: 'org_tools', tab_permissions: { crm: 'hidden' } }, + { id: 'ps_other', name: 'other_org_tools' }, + { id: 'ps_lapsed', name: 'lapsed_set' }, + { id: 'ps_dead', name: 'dead_set', active: false }, + ], + sys_position: [], + }, + }, + { + // TENANT_ADMIN rung: the org-admin capability, held through a position, + // with a tab merge across two sets (highest visibility wins). + name: 'tenant-admin-via-position', + userId: 'u_ta', + opts: { tenantId: 'org_a', seedEmail: 'ta@x.com', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_ta' }], + sys_member: [{ user_id: 'u_ta', organization_id: 'org_a', role: 'admin' }], + sys_user_position: [], + sys_user_permission_set: [{ user_id: 'u_ta', permission_set_id: 'ps_low', organization_id: null }], + sys_position: [{ id: 'p_orgadmin', name: 'org_admin' }, { id: 'p_everyone', name: 'everyone' }], + sys_position_permission_set: [{ position_id: 'p_orgadmin', permission_set_id: 'ps_oa' }], + sys_permission_set: [ + { id: 'ps_low', name: 'low', tab_permissions: { crm: 'default_off' } }, + { id: 'ps_oa', name: 'organization_admin', tab_permissions: { crm: 'default_on' } }, + ], + }, + }, + { + // The `ai_seat` read: no seedEmail, so BOTH the `current_user.email` + // fallback and the ADR-0024 seat synthesis need `sys_user` — and it must + // still be read exactly ONCE (the #2409 memo). + name: 'ai-seat-and-email-from-sys-user', + userId: 'u_ai', + opts: { tenantId: 'org_a', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_ai', email: 'ai@x.com', ai_access: 1 }], + sys_member: [{ user_id: 'u_ai', organization_id: 'org_a', role: 'member' }], + sys_user_position: [], + sys_user_permission_set: [], + }, + }, + { + // `ai_access` falsy → NO seat, and the email fallback still lands. The + // negative half of the read above: a batch that read the row but stopped + // consulting the flag would pass the fixture above and fail this one. + name: 'ai-seat-denied', + userId: 'u_noai', + opts: { nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_noai', email: 'noai@x.com', ai_access: 0 }], + sys_member: [], sys_user_position: [], sys_user_permission_set: [], + }, + }, + { + // Caller-seeded principal (the API-key shape): seeded scopes come FIRST and + // in order, the seeded email wins over `sys_user`, and because `ai_seat` is + // already held the seat read must NOT be issued at all. + name: 'seeded-permissions-and-email', + userId: 'u_seed', + opts: { + tenantId: 'org_a', + seedEmail: 'seed@x.com', + seedPermissions: ['key_scope_b', 'key_scope_a', 'ai_seat'], + nowMs: T0, + }, + tables: { + sys_user: [{ id: 'u_seed', email: 'ignored@x.com', ai_access: 1 }], + sys_member: [{ user_id: 'u_seed', organization_id: 'org_a', role: 'member' }], + sys_user_position: [], + sys_user_permission_set: [{ user_id: 'u_seed', permission_set_id: 'ps_x', organization_id: null }], + sys_permission_set: [{ id: 'ps_x', name: 'extra' }], + sys_position: [], + }, + }, + { + // Limits interacting with the batch: 205 own memberships against the 200 + // limit, 1005 peers against the 1000 limit. Truncation is OBSERVABLE here + // (the double slices like a driver), so a batch that changed either limit — + // or merged the two reads under one of them — moves these arrays. + name: 'read-limits-truncate', + userId: 'u_lim', + opts: { tenantId: 'org_0000', seedEmail: 'lim@x.com', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_lim' }], + sys_member: [...manyOwnMemberships, ...manyPeers], + sys_user_position: [], + sys_user_permission_set: [], + }, + }, + { + // No active organization: every membership contributes its role (the + // pre-ADR-0105-D2 org-less behaviour) and the fellow-org read is skipped + // entirely — one fewer query, and the batch must skip it too. + name: 'no-active-org', + userId: 'u_noorg', + opts: { seedEmail: 'noorg@x.com', nowMs: T0 }, + tables: { + sys_user: [{ id: 'u_noorg' }], + sys_member: [ + { user_id: 'u_noorg', organization_id: 'org_a', role: 'owner' }, + { user_id: 'u_noorg', organization_id: 'org_b', role: 'member,admin' }, + ], + sys_user_position: [], + sys_user_permission_set: [], + }, + }, +]; + +// ── Goldens captured from the SEQUENTIAL implementation ───────────────────── + +interface Golden { sequentialLegs: number; grants: unknown; queries: RecordedCall[] } + +/** + * Captured by running the fixtures above against + * `git show 38bc74ed1:packages/core/src/security/resolve-authz-context.ts` + * — the last commit before the batch. `sequentialLegs` is that run's leg + * count, which equalled its query count because every read awaited the one + * before it. + */ +const GOLDEN: Record = JSON.parse( + readFileSync(new URL('./resolve-authz-context.batch-equivalence.golden.json', import.meta.url), 'utf8'), +); + +/** + * The leg count each fixture resolves in AFTER batching — the changed-behaviour + * control, written out per fixture rather than derived, so a regression that + * re-serialises one read shows up as a number rather than as a slower suite. + * + * Four is the floor, not three: wave 1 (every independent read) → wave 2 + * (`sys_position`, needs the position NAMES wave 1 produced) → wave 3 + * (`sys_position_permission_set`, needs the position IDS wave 2 produced) → + * wave 4 (`sys_permission_set`, needs the union of directly- and + * position-granted ids). A principal with no `sys_position` row backing any of + * its position names skips wave 3 and lands in 3. + */ +const BATCHED_LEGS: Record = { + 'empty-principal': 2, + 'multi-org-membership': 2, + 'lapsed-own-membership-among-active-peers': 2, + 'position-derived-grants': 4, + 'permission-set-derived-grants': 3, + 'tenant-admin-via-position': 4, + 'ai-seat-and-email-from-sys-user': 2, + 'ai-seat-denied': 2, + 'seeded-permissions-and-email': 3, + 'read-limits-truncate': 2, + 'no-active-org': 2, +}; + +/** Order-insensitive comparison key: a batch reorders ISSUE order by design. */ +const asMultiset = (calls: RecordedCall[]) => calls.map((c) => JSON.stringify(c)).sort(); + +describe('[#10825] batched resolveUserAuthzGrants — equivalence with the sequential reads', () => { + it('the fixture matrix and the captured goldens have not drifted apart', () => { + expect(FIXTURES.map((f) => f.name).sort()).toEqual(Object.keys(GOLDEN).sort()); + expect(Object.keys(BATCHED_LEGS).sort()).toEqual(Object.keys(GOLDEN).sort()); + }); + + describe.each(FIXTURES.map((f) => [f.name, f] as const))('%s', (name, f) => { + it('resolves the SAME grants envelope, row for row and in the same order', async () => { + const ql = makeRecordingQl(f.tables); + const grants = await resolveUserAuthzGrants(ql, f.userId, f.opts); + expect(grants).toEqual(GOLDEN[name].grants); + }); + + it('issues the SAME reads — same objects, same filters, same tenancy scoping, same limits', async () => { + const ql = makeRecordingQl(f.tables); + await resolveUserAuthzGrants(ql, f.userId, f.opts); + // Multiset, not sequence: parallelising is precisely a change of issue + // order. What must not change is WHICH reads happen and with WHAT. + expect(asMultiset(ql.calls)).toEqual(asMultiset(GOLDEN[name].queries)); + // Batching must not become a privilege change by another route: every + // read still runs as system, as it did before. + expect(ql.calls.every((c) => c.isSystem)).toBe(true); + // Query COUNT is not the win here and must not silently become one: + // an extra read would mean the batch speculated, a missing one would + // mean it elided a read the sequential path made. + expect(ql.calls.length).toBe(GOLDEN[name].queries.length); + }); + + it('collapses those reads into fewer sequential LEGS — the actual win', async () => { + const ql = makeRecordingQl(f.tables); + await resolveUserAuthzGrants(ql, f.userId, f.opts); + expect(ql.legs).toBe(BATCHED_LEGS[name]); + expect(ql.legs).toBeLessThan(GOLDEN[name].sequentialLegs); + // Every wave-1 read really is in wave 1 — a `Promise.all` that someone + // later `await`s member-by-member would still pass the two assertions + // above while restoring the whole cost this card removed. + const wave1 = ql.calls.filter((_, i) => ql.legOf[i] === 1).map((c) => c.object).sort(); + expect(wave1).toEqual( + asMultiset(GOLDEN[name].queries) + .map((s) => JSON.parse(s) as RecordedCall) + .filter((c) => c.object !== 'sys_position' && c.object !== 'sys_position_permission_set' && c.object !== 'sys_permission_set') + .map((c) => c.object) + .sort(), + ); + }); + }); +}); diff --git a/packages/core/src/security/resolve-authz-context.ts b/packages/core/src/security/resolve-authz-context.ts index cd376a0f38..647b5dae74 100644 --- a/packages/core/src/security/resolve-authz-context.ts +++ b/packages/core/src/security/resolve-authz-context.ts @@ -332,6 +332,49 @@ export async function resolveUserAuthzGrants( return userRow; }; + // Single clock for every validity-window check in this resolution + // (ADR-0091 D2 — a grant row outside [valid_from, valid_until) does not + // resolve, fail-closed, with no background job involved). + const nowMs = opts.nowMs ?? Date.now(); + + // ── WAVE 1 [#10825] ──────────────────────────────────────────────────── + // The reads below share NO data dependency with one another, so they are + // issued TOGETHER: one sequential leg instead of five. cloud#1539 measured + // (latency injection, R² = 0.9994) that an authenticated request's cost is + // driven by its count of sequential LEGS, not by its count of queries — + // `server_ms ≈ 33 + L × 36.6` — so parallelising reads is worth exactly as + // much as deleting them, at none of the risk. + // + // ⛔ Parallelising is the ONLY thing that changed. Every read keeps its own + // object, its own `where` (including tenancy scoping) and its own `limit`, + // and every result is consumed in the same order as before, because on an + // authorization path a batch that returns different rows is a privilege bug + // no "the request still succeeds" test can see. In particular the two + // `sys_member` reads are NOT merged into one `$or` read that partitions in + // memory: they answer different questions under different limits (200 vs + // 1000), and a merged read would feed OTHER members' rows to the + // `accessible_org_ids` / role loops below — silently granting this caller + // their org access and their org-admin role. + // `resolve-authz-context.batch-equivalence.test.ts` is the differential + // control for all of this: the whole envelope and the exact multiset of + // issued `{object, where, limit}` triples, captured from the sequential + // implementation and asserted against this one. + // + // `sys_user` joins the wave only when it will actually be consulted — the + // #2409 memo below still guarantees it is read at most once, and a caller + // that already supplied both an email and the `ai_seat` scope still causes + // no read at all. + const needsUserRow = !grants.email || !grants.permissions.includes('ai_seat'); + const [members, userPositionRows, orgMembers, upsRowsAll] = await Promise.all([ + tryFind(ql, 'sys_member', { user_id: userId }, 200), + tryFind(ql, 'sys_user_position', { user_id: userId }, 200), + tenantId + ? tryFind(ql, 'sys_member', { organization_id: tenantId }, 1000) + : Promise.resolve([] as any[]), + tryFind(ql, 'sys_user_permission_set', { user_id: userId }, 100), + needsUserRow ? getUserRow() : Promise.resolve(undefined), + ]); + // Resolve the caller's unique email for `current_user.email` RLS owner // policies when the caller didn't supply it (e.g. API-key auth). if (!grants.email) { @@ -339,11 +382,6 @@ export async function resolveUserAuthzGrants( if (u?.email) grants.email = String(u.email); } - // Single clock for every validity-window check in this resolution - // (ADR-0091 D2 — a grant row outside [valid_from, valid_until) does not - // resolve, fail-closed, with no background job involved). - const nowMs = opts.nowMs ?? Date.now(); - // 3. Memberships via sys_member (better-auth). ONE read serves two purposes, // so the two facts can never disagree about what the user belongs to: // @@ -364,7 +402,7 @@ export async function resolveUserAuthzGrants( // columns are absent on `sys_member` today, and `isGrantActive` treats // an absent bound as unbounded, so this is a no-op until they exist and // correct the moment they do. - const members = await tryFind(ql, 'sys_member', { user_id: userId }, 200); + // (Read in WAVE 1 above.) const accessibleOrgIds = new Set(); for (const m of members) { if (!isGrantActive(m, nowMs)) continue; @@ -392,7 +430,7 @@ export async function resolveUserAuthzGrants( // 4. [ADR-0057 D4] Platform-owned RBAC role assignments (sys_user_position) — the // source of truth for custom roles, decoupled from sys_member.role. // `organization_id = null` = global (cross-tenant); else match active org. - const userPositionRows = await tryFind(ql, 'sys_user_position', { user_id: userId }, 200); + // (Read in WAVE 1 above.) for (const ur of userPositionRows) { const org = ur.organization_id ?? null; if (org && tenantId && org !== tenantId) continue; @@ -402,8 +440,9 @@ export async function resolveUserAuthzGrants( } // 5. Fellow-org user IDs so RLS can scope identity tables to collaborators. + // (Read in WAVE 1 above — issued only when there IS an active org, and + // scoped to it there exactly as it was here.) if (tenantId) { - const orgMembers = await tryFind(ql, 'sys_member', { organization_id: tenantId }, 1000); const ids = new Set( orgMembers .map((m) => m.user_id ?? m.userId) @@ -416,7 +455,7 @@ export async function resolveUserAuthzGrants( // 6. Permission sets — user-scoped grants (null org = global, else active org). // Rows outside their validity window are dropped BEFORE any derivation, so // an expired admin_full_access grant cannot yield platform_admin either. - const upsRowsAll = await tryFind(ql, 'sys_user_permission_set', { user_id: userId }, 100); + // (Read in WAVE 1 above.) const upsRows = upsRowsAll.filter((r) => isGrantActive(r, nowMs)); const psIds = new Set( upsRows @@ -461,6 +500,15 @@ export async function resolveUserAuthzGrants( // Only a name whose row is explicitly deactivated is dropped — a name // with no `sys_position` row at all (`org_owner`, a membership-derived // role) has no flag to read and is untouched. + // + // ── WAVE 2 / WAVE 3 [#10825] ────────────────────────────────────── + // These two reads are the resolver's only genuine chain: the position + // NAMES are not known until wave 1 answers, and the junction table keys + // on `position_id`, so the id lookup cannot be issued until + // `sys_position` has answered. Nothing else is left to run beside them, + // so four waves — not the card's hoped-for two or three — is the floor + // reachable without a join/traversal the generic `ql` seam cannot + // guarantee across engines (see the test file's leg table). if (grants.positions.length > 0) { const positionRows = await tryFind(ql, 'sys_position', { name: { $in: grants.positions } }, 100); const deactivatedNames = new Set( @@ -481,6 +529,12 @@ export async function resolveUserAuthzGrants( // 6b. Resolve permission-set details (names → grants.permissions; system_permissions; // tab_permissions merged by highest visibility). + // + // ── WAVE 4 [#10825] ─────────────────────────────────────────────── + // Deliberately ONE read over the UNION of directly-granted and + // position-granted set ids, not two reads issued as they become known: + // `grants.permissions` is appended in the order the driver returns these + // rows, and splitting the `$in` would reorder it. if (psIds.size > 0) { const psRowsAll = await tryFind(ql, 'sys_permission_set', { id: { $in: Array.from(psIds) } }, 500); // [ADR-0049] A DEACTIVATED permission set grants nothing — the From b3f2c26eb813aba71b71af4e60f803f5051ce3c8 Mon Sep 17 00:00:00 2001 From: Jack Zhuang Date: Sat, 22 Aug 2026 09:53:56 +0800 Subject: [PATCH 2/2] docs(changeset): batch resolveUserAuthzGrants' independent reads (#10825) --- .changeset/batch-authz-grant-legs.md | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .changeset/batch-authz-grant-legs.md diff --git a/.changeset/batch-authz-grant-legs.md b/.changeset/batch-authz-grant-legs.md new file mode 100644 index 0000000000..e78ea812d0 --- /dev/null +++ b/.changeset/batch-authz-grant-legs.md @@ -0,0 +1,46 @@ +--- +"@objectstack/core": patch +--- + +perf(core): batch `resolveUserAuthzGrants`' independent reads — 8 sequential legs become 4 (#10825) + +`resolveUserAuthzGrants` is legs 6–13 of every authenticated request. It read +`sys_member`, `sys_user_position`, `sys_member` (fellow-org peers), +`sys_user_permission_set`, `sys_position`, `sys_position_permission_set`, +`sys_permission_set` and `sys_user` **one after another**, each `await` +blocking the next, although only three of those seven edges are real data +dependencies. + +cloud#1539 measured causally (latency injection, R² = 0.9994) that an +authenticated request's server time follows `≈ 33 + L × 36.6` ms, where `L` is +the count of **sequential legs** — not the count of queries. Batching is +therefore worth exactly as much as deleting, at none of the risk. + +The reads that depend on nothing are now issued together, leaving one genuine +chain (`sys_position` → `sys_position_permission_set` → `sys_permission_set`, +which needs position names, then position ids, then the union of set ids): + +| | queries | sequential legs | +| --- | --- | --- | +| before | 8 | 8 | +| after | 8 | **4** (3 with no active `sys_position` row, 2 with no permission sets) | + +Measured by latency injection at 25 ms and 50 ms per query — 412 ms → 206 ms at +D = 50 — and independently by a leg-counting engine double across an 11-shape +fixture matrix. + +**No caching. Nothing survives a request.** Every read is still live, so a grant +revoked at T is honoured at T; there is no TTL, no invalidation contract and no +staleness window. That is the whole reason this is separable from #10757's +caching tranche. + +**Query count, filters, limits and tenancy scoping are unchanged — proven, not +asserted.** On an authorization path a batch that returns even one different row +is a privilege bug a green suite cannot see, so the two `sys_member` reads are +deliberately **not** merged into one `$or` read partitioned in memory: they +answer different questions under different limits (200 vs 1000), and a merge +would feed other members' rows to the `accessible_org_ids` and org-role loops. +`resolve-authz-context.batch-equivalence.test.ts` pins the whole resolved +envelope and the exact multiset of issued `{object, where, limit}` triples, +captured from the sequential implementation and asserted against the batched +one.