feat: add platform staff management

This commit is contained in:
Codex
2026-06-30 08:28:44 +08:00
parent 780feee4f8
commit 4f64b7aed8
21 changed files with 1205 additions and 25 deletions

View File

@@ -22,6 +22,7 @@ const AUTH_USER_ID = '00000000-0000-0000-0000-00000000a101';
const AUTH_TENANT_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a102';
const AUTH_PLATFORM_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a999';
const AUTH_RESTRICTED_PLATFORM_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a998';
const AUTH_PLATFORM_STAFF_USER_ID = '00000000-0000-4000-8000-00000000a997';
const AUTH_JWT_SECRET = 'development-jwt-secret-change-me';
const START_SERVER = process.argv.includes('--start-server');
const ENABLE_REAL_STORAGE_SIGN_TESTS = process.env.ENABLE_REAL_STORAGE_SIGN_TESTS === 'true';
@@ -1179,6 +1180,247 @@ async function testPlatformAdminPermissions() {
assert.equal(paymentDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'manual service-fee payment must require billing payment permission');
}
async function testPlatformStaffManagement() {
const adminHeaders = { 'x-platform-admin-key': 'local-platform-admin-key' };
const staffSuffix = Date.now().toString(36);
const staffEmail = `platform.staff.${staffSuffix}@example.test`;
const staffPhone = `139${String(Date.now()).slice(-8)}`;
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
try {
await pool.query(
`
insert into auth.users (
id, aud, role, phone, email, phone_confirmed_at, email_confirmed_at,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
)
values (
$1, 'authenticated', 'authenticated', $2, $3,
now(), now(),
'{"provider":"phone","providers":["phone"],"app_role":"platform_admin"}'::jsonb,
'{}'::jsonb, now(), now()
)
on conflict (id)
do update set phone = excluded.phone,
email = excluded.email,
raw_app_meta_data = excluded.raw_app_meta_data,
updated_at = now()
`,
[AUTH_PLATFORM_STAFF_USER_ID, staffPhone, staffEmail],
);
} finally {
await pool.end();
}
const created = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PUT',
body: {
authUserId: AUTH_PLATFORM_STAFF_USER_ID,
username: 'platform_staff_operator',
email: staffEmail,
phone: staffPhone,
name: 'Platform Staff Operator',
status: 'active',
platformPermissions: {
'platform:staff:read': true,
'platform:overview:read': true,
'platform:tenant:read': true,
},
metadata: {
title: 'operations',
secretToken: 'should-not-be-returned-through-audit',
},
},
});
assert.ok(created.item?.id, 'platform admin should create platform staff');
assert.equal(created.item?.platformPermissions?.['platform:tenant:read'], true, 'platform staff response should include granted permissions');
assert.ok(!JSON.stringify(created).includes('local-platform-admin-key'), 'platform staff response must not leak platform key');
const invalidPermission = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PUT',
body: {
authUserId: AUTH_PLATFORM_STAFF_USER_ID,
username: 'invalid_platform_staff',
name: 'Invalid Platform Staff',
platformPermissions: {
'platform:unknown:write': true,
},
},
expectStatus: 400,
});
assert.equal(invalidPermission.code, 'INVALID_PLATFORM_PERMISSION', 'platform staff permissions should reject unknown permission keys');
const missingAuthUser = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PUT',
body: {
authUserId: '00000000-0000-4000-8000-00000000dead',
username: 'missing_auth_platform_staff',
name: 'Missing Auth Platform Staff',
platformPermissions: {
'platform:overview:read': true,
},
},
expectStatus: 404,
});
assert.equal(missingAuthUser.code, 'AUTH_USER_NOT_FOUND', 'platform staff must bind an existing Supabase Auth user');
const missingAuthBinding = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PUT',
body: {
username: 'unbound_platform_staff',
name: 'Unbound Platform Staff',
platformPermissions: {
'platform:overview:read': true,
},
},
expectStatus: 400,
});
assert.equal(missingAuthBinding.code, 'REQUIRED_FIELD', 'platform staff authUserId should be required');
const emptyPermissionDenied = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PUT',
body: {
authUserId: AUTH_PLATFORM_STAFF_USER_ID,
username: 'empty_permission_platform_staff',
name: 'Empty Permission Platform Staff',
status: 'active',
platformPermissions: {},
},
expectStatus: 400,
});
assert.equal(emptyPermissionDenied.code, 'PLATFORM_PERMISSION_EMPTY', 'active platform staff should require explicit permissions');
const staffList = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { q: 'platform_staff_operator' },
});
assert.ok(staffList.items?.some(item => item.id === created.item.id), 'platform staff list should include created staff');
const staffJwt = await createSupabaseJwt(AUTH_PLATFORM_STAFF_USER_ID, {
phone: staffPhone,
appRole: 'platform_admin',
tenantId: false,
});
const staffHeaders = { authorization: `Bearer ${staffJwt}` };
const staffPermissions = await request('/api/platform-admin/permissions', {
tenantId: false,
userId: false,
headers: staffHeaders,
});
assert.equal(staffPermissions.item?.effective?.['platform:staff:read'], true, 'staff should expose its staff read permission');
assert.equal(staffPermissions.item?.effective?.['platform:tenant:write'], false, 'staff should not expose ungranted tenant write permission');
const staffCanListTenants = await request('/api/platform-admin/tenants', {
tenantId: false,
userId: false,
headers: staffHeaders,
});
assert.ok(Array.isArray(staffCanListTenants.items), 'staff should list tenants with tenant read permission');
const staffCreateTenantDenied = await request('/api/platform-admin/tenants', {
tenantId: false,
userId: false,
headers: staffHeaders,
method: 'POST',
body: {
slug: `staff-denied-${Date.now().toString(36)}`,
name: 'Staff Denied Tenant',
},
expectStatus: 403,
});
assert.equal(staffCreateTenantDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'platform staff should not create tenants without tenant write permission');
const staffWriteDenied = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: staffHeaders,
method: 'PUT',
body: {
username: 'staff_cannot_create_staff',
name: 'Staff Cannot Create Staff',
platformPermissions: { 'platform:overview:read': true },
},
expectStatus: 403,
});
assert.equal(staffWriteDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'staff write should require explicit platform staff write permission');
const platformAdminJwt = await createSupabaseJwt(AUTH_PLATFORM_ADMIN_USER_ID, {
phone: '13999999999',
appRole: 'platform_admin',
tenantId: false,
});
const platformAdminHeaders = { authorization: `Bearer ${platformAdminJwt}` };
const selfDowngradeDenied = await request('/api/platform-admin/staff', {
tenantId: false,
userId: false,
headers: platformAdminHeaders,
method: 'PUT',
body: {
id: '00000000-0000-0000-0000-000000000999',
authUserId: AUTH_PLATFORM_ADMIN_USER_ID,
username: 'smoke_platform_admin',
phone: '13999999999',
name: 'Smoke Platform Admin',
status: 'active',
platformPermissions: { 'platform:overview:read': true },
},
expectStatus: 400,
});
assert.equal(selfDowngradeDenied.code, 'CANNOT_DOWNGRADE_SELF', 'platform admin should not remove its own super permission');
const disabled = await request('/api/platform-admin/staff/status', {
tenantId: false,
userId: false,
headers: adminHeaders,
method: 'PATCH',
body: {
staffId: created.item.id,
status: 'disabled',
reason: 'integration disable test',
revokeSessions: true,
},
});
assert.equal(disabled.item?.status, 'disabled', 'platform admin should disable platform staff');
const disabledStaffDenied = await request('/api/platform-admin/overview', {
tenantId: false,
userId: false,
headers: staffHeaders,
expectStatus: 403,
});
assert.equal(disabledStaffDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'disabled platform staff JWT should no longer authenticate as platform admin');
const audit = await request('/api/platform-admin/audit-logs', {
tenantId: false,
userId: false,
headers: adminHeaders,
query: { targetType: 'platform_user', q: 'platform.staff', limit: 20 },
});
assert.ok(
audit.items?.some(item => item.targetId === created.item.id && item.action === 'platform.staff.status_updated'),
'platform staff status changes should be audited',
);
assert.ok(!JSON.stringify(audit).includes('should-not-be-returned-through-audit'), 'platform staff audit should not leak token-like metadata');
}
async function testSupabaseJwksIdentity() {
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
const publicJwk = await exportJWK(publicKey);
@@ -9014,6 +9256,7 @@ async function main() {
await check('phone binding', testPhoneBinding);
await check('Supabase JWT identity', testSupabaseJwtIdentity);
await check('platform admin permissions', testPlatformAdminPermissions);
await check('platform staff management', testPlatformStaffManagement);
await check('Supabase JWKS JWT identity', testSupabaseJwksIdentity);
await check('legacy auth headers disabled', testLegacyAuthHeadersDisabled);
await check('platform tenant operations and audit', testPlatformTenantOperationsAndAudit);

View File

@@ -83,11 +83,44 @@ function runWorkerOnce() {
}
async function cleanup(pool) {
await pool.query(
`
delete from public.platform_audit_notification_events
where channel_id in (
select id
from public.platform_audit_notification_channels
where channel_code in ('worker_platform_audit_test', 'integration_platform_audit')
)
or alert_id in (
select id
from public.platform_audit_alerts
where details->>'source' in ('worker-test', 'api-integration-test')
or details->>'workerId' = 'platform-audit-alert-worker-test'
)
`,
);
await pool.query("delete from public.platform_audit_notification_events where alert_id = $1", [ids.alert]);
await pool.query("delete from public.platform_audit_notification_channels where channel_code = 'worker_platform_audit_test'");
await pool.query("delete from app_private.platform_secrets where secret_scope = 'webhook' and secret_key = 'worker_platform_audit_test'");
await pool.query('delete from public.platform_audit_alerts where id = $1 or audit_log_id = $2', [ids.alert, ids.auditLog]);
await pool.query('delete from public.audit_logs where id = $1 or tenant_id = $2', [ids.auditLog, ids.tenant]);
await pool.query(
`
delete from public.platform_audit_alerts
where id = $1
or audit_log_id = $2
or details->>'source' in ('worker-test', 'api-integration-test')
or details->>'workerId' = 'platform-audit-alert-worker-test'
`,
[ids.alert, ids.auditLog],
);
await pool.query(
`
delete from public.audit_logs
where id = $1
or tenant_id = $2
or user_agent in ('platform-audit-notification-worker-test', 'platform-audit-alert-worker-test')
`,
[ids.auditLog, ids.tenant],
);
await pool.query('delete from public.tenant_billing_profiles where tenant_id = $1', [ids.tenant]);
await pool.query('delete from public.tenant_domains where tenant_id = $1', [ids.tenant]);
await pool.query('delete from public.tenants where id = $1', [ids.tenant]);
@@ -108,7 +141,7 @@ async function seed(pool, webhookUrl) {
details, ip_address, user_agent, created_at
)
values (
$1, $2::uuid, null, 'platform.tenant.status_updated', 'tenant', $3,
$1, $2::uuid, null, 'platform.worker_test.status_updated', 'tenant', $3,
'{"status":"suspended","apiKey":"must-not-leak","nested":{"password":"must-not-leak"}}'::jsonb,
'127.0.0.1', 'platform-audit-notification-worker-test', now()
)
@@ -125,7 +158,7 @@ async function seed(pool, webhookUrl) {
)
values (
$1, $2, $3, $4::uuid, 'high', 'open',
'platform.tenant.status_updated', 'tenant', $4,
'platform.worker_test.status_updated', 'tenant', $4,
'租户状态变更告警', 'platform audit notification integration alert',
'{"source":"worker-test","apiKey":"must-not-leak","nested":{"password":"must-not-leak"}}'::jsonb,
now(), now()
@@ -153,7 +186,7 @@ async function seed(pool, webhookUrl) {
values (
'worker_platform_audit_test', 'Worker 平台审计通知', true, 'generic',
$1, 'app_private.platform_secrets:webhook:worker_platform_audit_test',
'medium', array['open']::text[], array['platform.tenant.*']::text[], 5
'medium', array['open']::text[], array['platform.worker_test.*']::text[], 5
)
on conflict (channel_code)
do update set enabled = excluded.enabled,

View File

@@ -394,10 +394,47 @@ async function validateDatabase() {
pass('db.provider_public_config', 'Active provider public configs do not contain secret-like keys');
}
const activePlatformAdminRows = await pool.query(`
select id, username, phone, auth_user_id
from public.platform_users
where primary_role = 'platform_admin'
and status = 'active'
order by created_at asc
limit 20
`);
if (activePlatformAdminRows.rowCount === 0) {
block('db.platform_admin_active', 'At least one active platform admin user is required');
} else {
pass('db.platform_admin_active', 'Active platform admin users found', { count: activePlatformAdminRows.rowCount });
}
const activePlatformAdminsWithoutAuth = await pool.query(`
select id, username, phone
from public.platform_users
where primary_role = 'platform_admin'
and status = 'active'
and auth_user_id is null
order by created_at asc
limit 20
`);
if (activePlatformAdminsWithoutAuth.rowCount > 0) {
block('db.platform_admin_auth_binding', 'Active platform admin users must be bound to Supabase Auth users', {
count: activePlatformAdminsWithoutAuth.rowCount,
samples: activePlatformAdminsWithoutAuth.rows.map(row => ({
id: row.id,
username: row.username,
phone: row.phone ? `${String(row.phone).slice(0, 3)}****${String(row.phone).slice(-4)}` : null,
})),
});
} else {
pass('db.platform_admin_auth_binding', 'Active platform admin users are bound to Supabase Auth users');
}
const platformAdminsWithoutPermissions = await pool.query(`
select id, username, phone
from public.platform_users
where primary_role = 'platform_admin'
and status = 'active'
and (platform_permissions is null or platform_permissions = '{}'::jsonb)
order by created_at asc
limit 20
@@ -415,6 +452,32 @@ async function validateDatabase() {
pass('db.platform_admin_permissions', 'Platform admin users have explicit permission maps');
}
const disabledPlatformAdminsWithActiveSessions = await pool.query(`
select u.id, u.username, u.phone, count(s.id)::int as active_session_count
from public.platform_users u
join app_private.auth_sessions s on s.user_id = u.id
where u.primary_role = 'platform_admin'
and u.status = 'disabled'
and s.revoked_at is null
and s.expires_at > now()
group by u.id, u.username, u.phone
order by active_session_count desc
limit 20
`);
if (disabledPlatformAdminsWithActiveSessions.rowCount > 0) {
block('db.platform_admin_disabled_sessions', 'Disabled platform admin users must not have active legacy sessions', {
count: disabledPlatformAdminsWithActiveSessions.rowCount,
samples: disabledPlatformAdminsWithActiveSessions.rows.map(row => ({
id: row.id,
username: row.username,
phone: row.phone ? `${String(row.phone).slice(0, 3)}****${String(row.phone).slice(-4)}` : null,
activeSessionCount: row.active_session_count,
})),
});
} else {
pass('db.platform_admin_disabled_sessions', 'Disabled platform admin users have no active legacy sessions');
}
const missingAuthSecretRows = await pool.query(`
select p.tenant_id, p.provider
from public.tenant_auth_providers p