Files
arrr-erp-test-v2/tests/api-auth-rbac.spec.js
T
2026-06-26 20:38:14 +05:30

531 lines
15 KiB
JavaScript

const { test, expect } = require('@playwright/test');
require('dotenv').config();
const { BASE_URL, absoluteUrl } = require('../fixtures/url');
const { login } = require('../fixtures/auth');
const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers');
const { expectBlockedOrSafe } = require('../fixtures/assertions');
async function safeGoto(page, route) {
const resp = await page.goto(route).catch(() => null);
await page.waitForLoadState('domcontentloaded').catch(() => {});
return resp;
}
async function expectRouteAvailable(page, resp) {
await expectNoBackendError(page);
if (resp) {
expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404);
expect(resp.status()).toBeLessThan(500);
}
}
async function expectNoSecrets(page) {
const body = await readBody(page);
expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i);
}
async function expectPublicNoSensitive(page) {
await expectNoBackendError(page);
const body = await readBody(page);
expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i);
}
async function apiCall(request, method, route) {
const url = absoluteUrl(route);
const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } };
if (method === 'GET') return await request.get(url).catch(() => null);
if (method === 'POST') return await request.post(url, opts).catch(() => null);
if (method === 'PUT') return await request.put(url, opts).catch(() => null);
if (method === 'DELETE') return await request.delete(url).catch(() => null);
if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null);
return await request.fetch(url, { method }).catch(() => null);
}
async function expectApiSafe(resp) {
expect(resp, 'API response should be available').toBeTruthy();
expect(resp.status()).toBeLessThan(500);
const text = await resp.text().catch(() => '');
expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i);
}
const cases = [
{
"variantId": "V251-API-001",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API auth token rejects blank credentials",
"type": "VAPT",
"route": "/api/auth/token",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-002",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API refresh rejects missing refresh token",
"type": "VAPT",
"route": "/api/auth/refresh",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-003",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API logout without token handled safely",
"type": "VAPT",
"route": "/api/auth/logout",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-004",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API me requires authentication",
"type": "VAPT",
"route": "/api/auth/me",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-005",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API forgot-password rejects invalid email safely",
"type": "VAPT",
"route": "/api/auth/forgot-password",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-006",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API reset-password rejects bogus token",
"type": "VAPT",
"route": "/api/auth/reset-password",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-007",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API invite accept rejects bogus token",
"type": "VAPT",
"route": "/api/auth/invite/accept",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-008",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API users requires authorization",
"type": "VAPT",
"route": "/api/users",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-009",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API users create CSRF/auth rejected",
"type": "VAPT",
"route": "/api/users",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-010",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API RBAC roles requires authorization",
"type": "VAPT",
"route": "/api/rbac/roles",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-011",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API RBAC roles create auth rejected",
"type": "VAPT",
"route": "/api/rbac/roles",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-012",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API RBAC permissions requires authorization",
"type": "VAPT",
"route": "/api/rbac/permissions",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-013",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API tenancy tenants requires authorization",
"type": "VAPT",
"route": "/api/tenancy/tenants",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-014",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API tenancy branches requires authorization",
"type": "VAPT",
"route": "/api/tenancy/branches",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-015",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API clients requires authorization",
"type": "VAPT",
"route": "/api/v1/clients",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-016",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API clients create auth rejected",
"type": "VAPT",
"route": "/api/v1/clients",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-017",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API client invalid ID safe",
"type": "VAPT",
"route": "/api/v1/clients/999999",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-018",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API client update invalid ID auth rejected",
"type": "VAPT",
"route": "/api/v1/clients/999999",
"_kind": "api",
"_method": "PUT",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-019",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API client delete invalid ID auth rejected",
"type": "VAPT",
"route": "/api/v1/clients/999999",
"_kind": "api",
"_method": "DELETE",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-020",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API cross-tenant query does not leak",
"type": "VAPT",
"route": "/api/v1/clients?tenant_id=999999",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-021",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API roles duplicate invalid POST safe",
"type": "VAPT",
"route": "/api/rbac/roles",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-022",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API permission elevation attempt rejected",
"type": "VAPT",
"route": "/api/rbac/roles/999999/permissions",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-023",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API tenant create auth rejected",
"type": "VAPT",
"route": "/api/tenancy/tenants",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-024",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API branch create auth rejected",
"type": "VAPT",
"route": "/api/tenancy/branches",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-025",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API malformed JSON safe",
"type": "VAPT",
"route": "/api/auth/token",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-026",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API token response does not leak password",
"type": "VAPT",
"route": "/api/auth/token",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-027",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API forgot password does not disclose account existence",
"type": "VAPT",
"route": "/api/auth/forgot-password",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-028",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API reset token replay rejected",
"type": "VAPT",
"route": "/api/auth/reset-password",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-029",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API rate-limit/bruteforce endpoint safe",
"type": "VAPT",
"route": "/api/auth/token",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-030",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API CORS preflight handled safely",
"type": "VAPT",
"route": "/api/auth/me",
"_kind": "api",
"_method": "OPTIONS",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-031",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API export clients requires authorization",
"type": "VAPT",
"route": "/api/v1/clients/export",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-032",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API users invalid ID safe",
"type": "VAPT",
"route": "/api/users/999999",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-033",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API users role update auth rejected",
"type": "VAPT",
"route": "/api/users/999999/roles",
"_kind": "api",
"_method": "POST",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-034",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API permission denied response does not include stack trace",
"type": "VAPT",
"route": "/api/users",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
},
{
"variantId": "V251-API-035",
"module": "API Auth / RBAC",
"role": "API/Anonymous",
"scenario": "API unknown endpoint returns safe 404",
"type": "VAPT",
"route": "/api/does-not-exist-uat-vapt",
"_kind": "api",
"_method": "GET",
"_file": "api-auth-rbac.spec.js",
"_login_role": null
}
];
test.describe("v2.5.1 Additions - API Auth / RBAC", () => {
for (const c of cases) {
test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => {
const role = c._login_role || c.role;
const kind = c._kind;
const method = c._method || 'GET';
if (kind === 'api') {
const resp = await apiCall(request, method, c.route);
await expectApiSafe(resp);
return;
}
if (kind === 'post') {
const resp = await apiCall(request, method || 'POST', c.route);
await expectApiSafe(resp);
const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status());
expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy();
return;
}
if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) {
await login(page, role);
}
const resp = await safeGoto(page, c.route);
if (kind === 'page') {
await expectRouteAvailable(page, resp);
if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page);
return;
}
if (kind === 'public') {
await expectRouteAvailable(page, resp);
return;
}
if (kind === 'public-no-sensitive') {
await expectPublicNoSensitive(page);
return;
}
if (kind === 'no-secret') {
await expectRouteAvailable(page, resp);
await expectNoSecrets(page);
return;
}
await expectNoBackendError(page);
await expectBlockedOrSafe(page, resp);
});
}
});