85 lines
3.5 KiB
JavaScript
85 lines
3.5 KiB
JavaScript
|
|
// share_cascade_rebac.js — measures ReBAC AuthZ cascade via a direct user
|
||
|
|
// grant. The seeder grants `read` on shared_subtree.root (depth 0) to the
|
||
|
|
// grantee user; this scenario times how long the grantee takes to fetch
|
||
|
|
// folders at varying depths inside that subtree.
|
||
|
|
//
|
||
|
|
// The AuthZ recursive-CTE in `pg_acl_engine` walks up the ancestor chain
|
||
|
|
// from the requested folder until it finds a matching grant (or runs out
|
||
|
|
// of ancestors). The deeper the requested folder, the more ancestors the
|
||
|
|
// CTE has to traverse before hitting the grant on the root:
|
||
|
|
//
|
||
|
|
// fetch_at_grant_root → 0 ancestors walked (grant is right here)
|
||
|
|
// fetch_as_grantee_depth4 → 4 ancestors walked
|
||
|
|
// fetch_as_grantee_depth8 → 8 ancestors walked
|
||
|
|
// fetch_as_grantee_depth_deep → `load_depth` ancestors walked
|
||
|
|
//
|
||
|
|
// A regression in AuthZ cost should show up as the cascade-depth curve
|
||
|
|
// flattening or steepening — that's the value of intermediate samples.
|
||
|
|
|
||
|
|
import { check } from 'k6';
|
||
|
|
import http from 'k6/http';
|
||
|
|
import { BASE, authParams } from '../lib/http.js';
|
||
|
|
import { login } from '../lib/auth.js';
|
||
|
|
import { thresholdsFromBaseline, loadManifest } from '../lib/metrics.js';
|
||
|
|
|
||
|
|
const manifest = loadManifest();
|
||
|
|
|
||
|
|
export const options = {
|
||
|
|
vus: 1,
|
||
|
|
// See folder_cascade.js for the iteration-count rationale: N=100 makes
|
||
|
|
// p99 a real percentile instead of the worst-of-the-batch.
|
||
|
|
iterations: 100,
|
||
|
|
thresholds: thresholdsFromBaseline('share_cascade_rebac'),
|
||
|
|
};
|
||
|
|
|
||
|
|
export function setup() {
|
||
|
|
const adminToken = login(manifest.admin.username, manifest.admin.password);
|
||
|
|
const granteeToken = login(manifest.grantee.username, manifest.grantee.password);
|
||
|
|
return { adminToken, granteeToken };
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function (data) {
|
||
|
|
const { adminToken, granteeToken } = data;
|
||
|
|
const t = manifest.shared_subtree;
|
||
|
|
|
||
|
|
// List grants on the granted folder (admin only).
|
||
|
|
const grantsRes = http.get(
|
||
|
|
`${BASE}/api/grants?resource_type=folder&resource_id=${t.root}`,
|
||
|
|
authParams(adminToken, 'share_cascade_rebac.list_grants'),
|
||
|
|
);
|
||
|
|
check(grantsRes, { 'list grants 200': (r) => r.status === 200 });
|
||
|
|
|
||
|
|
// Baseline: grantee fetches the folder the grant is directly on. AuthZ
|
||
|
|
// finds the matching grant on the first row of the CTE; zero ancestors
|
||
|
|
// walked. This metric measures the constant overhead of an authorized
|
||
|
|
// request — moves to the right of this metric is "cascade cost."
|
||
|
|
const d1 = http.get(
|
||
|
|
`${BASE}/api/folders/${t.root}/resources?resource_types=folder`,
|
||
|
|
authParams(granteeToken, 'share_cascade_rebac.fetch_as_grantee_depth1'),
|
||
|
|
);
|
||
|
|
check(d1, { 'fetch root 200': (r) => r.status === 200 });
|
||
|
|
|
||
|
|
// Grantee fetches a mid-tree folder. AuthZ walks 4 ancestors before
|
||
|
|
// hitting the grant.
|
||
|
|
const d4 = http.get(
|
||
|
|
`${BASE}/api/folders/${t.depth4}/resources?resource_types=folder`,
|
||
|
|
authParams(granteeToken, 'share_cascade_rebac.fetch_as_grantee_depth4'),
|
||
|
|
);
|
||
|
|
check(d4, { 'fetch depth4 200': (r) => r.status === 200 });
|
||
|
|
|
||
|
|
// Grantee fetches a folder 8 levels under the granted root. AuthZ
|
||
|
|
// walks 8 ancestors.
|
||
|
|
const d8 = http.get(
|
||
|
|
`${BASE}/api/folders/${t.depth8}/resources?resource_types=folder`,
|
||
|
|
authParams(granteeToken, 'share_cascade_rebac.fetch_as_grantee_depth8'),
|
||
|
|
);
|
||
|
|
check(d8, { 'fetch depth8 200': (r) => r.status === 200 });
|
||
|
|
|
||
|
|
// Grantee fetches the deepest descendant — full-length cascade.
|
||
|
|
const dD = http.get(
|
||
|
|
`${BASE}/api/folders/${t.deepest}/resources?resource_types=folder`,
|
||
|
|
authParams(granteeToken, 'share_cascade_rebac.fetch_as_grantee_depth_deep'),
|
||
|
|
);
|
||
|
|
check(dD, { 'fetch deepest 200': (r) => r.status === 200 });
|
||
|
|
}
|