@prosopo/user-access-policy 3.12.2 → 3.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$cjs.log +4 -4
- package/.turbo/turbo-build$colon$tsc.log +14 -14
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +30 -0
- package/dist/cjs/redis/reader/redisRulesReader.cjs +42 -11
- package/dist/cjs/redis/reader/redisRulesSplitQuery.cjs +5 -3
- package/dist/redis/reader/redisRulesReader.d.ts +1 -1
- package/dist/redis/reader/redisRulesReader.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesReader.js +43 -12
- package/dist/redis/reader/redisRulesReader.js.map +1 -1
- package/dist/redis/reader/redisRulesSplitQuery.d.ts +3 -0
- package/dist/redis/reader/redisRulesSplitQuery.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesSplitQuery.js +6 -4
- package/dist/redis/reader/redisRulesSplitQuery.js.map +1 -1
- package/dist/tests/redis/reader/redisRulesSplitQuery.unit.test.js +27 -1
- package/dist/tests/redis/reader/redisRulesSplitQuery.unit.test.js.map +1 -1
- package/dist/tests/redis/redisRulesReaderRank.benchmark.integration.test.js +1 -2
- package/dist/tests/redis/redisRulesReaderRank.benchmark.integration.test.js.map +1 -1
- package/dist/tests/redis/redisRulesStorage.integration.test.js +46 -0
- package/dist/tests/redis/redisRulesStorage.integration.test.js.map +1 -1
- package/package.json +7 -7
- package/src/redis/reader/redisRulesReader.ts +78 -28
- package/src/redis/reader/redisRulesSplitQuery.ts +31 -6
- package/src/tests/redis/reader/redisRulesSplitQuery.unit.test.ts +76 -1
- package/src/tests/redis/redisRulesReaderRank.benchmark.integration.test.ts +12 -4
- package/src/tests/redis/redisRulesStorage.integration.test.ts +100 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1629,6 +1629,106 @@ describe("redisAccessRulesStorage", () => {
|
|
|
1629
1629
|
),
|
|
1630
1630
|
).toBe(true);
|
|
1631
1631
|
});
|
|
1632
|
+
|
|
1633
|
+
test("specific-IP Restrict rule survives when many higher-specificity irrelevant rules co-exist (2026-07-10 Twickets regression)", async () => {
|
|
1634
|
+
// Regression guard for the 2026-07-10 Twickets incident: a
|
|
1635
|
+
// portal-authored Restrict rule with scope
|
|
1636
|
+
// `clientId + numericIp` (specificity 2, severity 0) was
|
|
1637
|
+
// silently dropped from the frictionless access-policy lookup
|
|
1638
|
+
// when the tenant had many higher-specificity irrelevant
|
|
1639
|
+
// rules on the same clientId. The old FT.AGGREGATE ranker
|
|
1640
|
+
// used for non-block lookups capped candidates at top-20 by
|
|
1641
|
+
// populated-field count; those 20 slots got filled by rules
|
|
1642
|
+
// with clientId + numericIp + numericIpMaskMin (SIMD_REPLAY
|
|
1643
|
+
// v6 shape, specificity 3, severity 1) or
|
|
1644
|
+
// clientId + ja4Hash + coords (SUDDEN_VOLUME_INCREASE shape,
|
|
1645
|
+
// specificity 3, severity 1) that all matched the greedy
|
|
1646
|
+
// `ismissing(@headHash) | ismissing(@coords) | ismissing(@headersHash)`
|
|
1647
|
+
// disjunction. None of them applied to the request under
|
|
1648
|
+
// `ruleApplies` — they were emitted from other users'
|
|
1649
|
+
// activity — so the JS-side ranker returned [] and the
|
|
1650
|
+
// frictionless decision fell through to `default_pow`.
|
|
1651
|
+
//
|
|
1652
|
+
// The split-query path can't hit that failure: each probe
|
|
1653
|
+
// hits a discriminating posting list, so the ip:exact probe
|
|
1654
|
+
// returns exactly the rules that literally match this IP.
|
|
1655
|
+
const clientId = getUniqueString();
|
|
1656
|
+
|
|
1657
|
+
// The rule that must survive: a portal "Too Many Requests"
|
|
1658
|
+
// Restrict/image rule scoped only to (clientId, numericIp).
|
|
1659
|
+
const targetIp = 1376899398n; // 82.17.209.70 — Twickets rule
|
|
1660
|
+
const targetRule: AccessRule = {
|
|
1661
|
+
type: AccessPolicyType.Restrict,
|
|
1662
|
+
clientId,
|
|
1663
|
+
numericIp: targetIp,
|
|
1664
|
+
description: "Too Many Requests",
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
// 40 irrelevant Block rules with higher specificity than
|
|
1668
|
+
// the target rule. Two shapes drawn from the live Twickets
|
|
1669
|
+
// tenant that dominated the top-20:
|
|
1670
|
+
//
|
|
1671
|
+
// (a) SIMD_REPLAY v6-style — clientId + numericIp +
|
|
1672
|
+
// numericIpMaskMin. These have a *different* numericIp
|
|
1673
|
+
// from the request, so the exact-IP probe skips them.
|
|
1674
|
+
// (b) SUDDEN_VOLUME_INCREASE-style — clientId + ja4Hash +
|
|
1675
|
+
// coords. Different ja4Hash from the request so those
|
|
1676
|
+
// probes miss them too.
|
|
1677
|
+
//
|
|
1678
|
+
// Under the old ranker every one of these would fill the top
|
|
1679
|
+
// slots and push the target rule off the end.
|
|
1680
|
+
const irrelevantRules: AccessRule[] = [];
|
|
1681
|
+
for (let i = 0; i < 20; i++) {
|
|
1682
|
+
irrelevantRules.push({
|
|
1683
|
+
type: AccessPolicyType.Block,
|
|
1684
|
+
clientId,
|
|
1685
|
+
numericIp: BigInt(2000000000 + i),
|
|
1686
|
+
numericIpMaskMin: BigInt(2000000000 + i),
|
|
1687
|
+
numericIpMaskMax: BigInt(2000000000 + i),
|
|
1688
|
+
description: "SIMD_REPLAY",
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
for (let i = 0; i < 20; i++) {
|
|
1692
|
+
irrelevantRules.push({
|
|
1693
|
+
type: AccessPolicyType.Block,
|
|
1694
|
+
clientId,
|
|
1695
|
+
ja4Hash: `t13d_other_${i}`,
|
|
1696
|
+
coords: `[[[${100 + i},${200 + i}]]]`,
|
|
1697
|
+
description: "SUDDEN_VOLUME_INCREASE",
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
await insertRules([targetRule, ...irrelevantRules]);
|
|
1702
|
+
|
|
1703
|
+
// Frictionless access-policy lookup shape — greedy match,
|
|
1704
|
+
// matchingFieldsOnly=true, no blockOnly.
|
|
1705
|
+
const found = await accessRulesReader.findRules(
|
|
1706
|
+
{
|
|
1707
|
+
policyScope: { clientId },
|
|
1708
|
+
policyScopeMatch: FilterScopeMatch.Greedy,
|
|
1709
|
+
userScope: {
|
|
1710
|
+
numericIp: targetIp,
|
|
1711
|
+
ja4Hash: "t13d1516h2_request_ja4",
|
|
1712
|
+
userAgentHash: "sha256:request_ua",
|
|
1713
|
+
userId: getUniqueString(),
|
|
1714
|
+
},
|
|
1715
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
1716
|
+
},
|
|
1717
|
+
true, // matchingFieldsOnly — production hot path
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
// The target rule MUST come back. Under the old ranker it
|
|
1721
|
+
// was truncated; under the split path the ip:exact probe
|
|
1722
|
+
// returns exactly this one rule, so it can't be crowded
|
|
1723
|
+
// out by irrelevant candidates from other probes.
|
|
1724
|
+
const targetFound = found.find(
|
|
1725
|
+
(r) =>
|
|
1726
|
+
r.type === AccessPolicyType.Restrict &&
|
|
1727
|
+
r.numericIp === targetIp &&
|
|
1728
|
+
r.description === "Too Many Requests",
|
|
1729
|
+
);
|
|
1730
|
+
expect(targetFound).toBeDefined();
|
|
1731
|
+
});
|
|
1632
1732
|
});
|
|
1633
1733
|
|
|
1634
1734
|
afterAll(async () => {
|