feat(M02-C): 建立RBAC与门店数据范围
This commit is contained in:
@@ -90,7 +90,16 @@ const auth = {
|
||||
},
|
||||
jwtSecret: secret,
|
||||
accessTokenTtlSeconds: 900,
|
||||
sessionTtlSeconds: 604800
|
||||
sessionTtlSeconds: 604800,
|
||||
accessControl: {
|
||||
async getAccessProfile() {
|
||||
return {
|
||||
roles: ['CUSTOMER'],
|
||||
capabilities: ['order.self.read', 'profile.read'],
|
||||
storeIds: []
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const app = await buildApp({ auth });
|
||||
@@ -114,6 +123,7 @@ const me = await app.inject({
|
||||
});
|
||||
assert.equal(me.statusCode, 200);
|
||||
assert.equal(me.json().data.user.id, '21');
|
||||
assert.deepEqual(me.json().data.access.roles, ['CUSTOMER']);
|
||||
|
||||
const logout = await app.inject({
|
||||
method: 'POST',
|
||||
|
||||
@@ -21,6 +21,9 @@ const tenantAppsVerifySql = read('database/migrations/2026061803_m02a_tenant_app
|
||||
const authUpSql = read('database/migrations/2026061804_m02b_wechat_auth.up.sql');
|
||||
const authDownSql = read('database/migrations/2026061804_m02b_wechat_auth.down.sql');
|
||||
const authVerifySql = read('database/migrations/2026061804_m02b_wechat_auth.verify.sql');
|
||||
const rbacUpSql = read('database/migrations/2026061805_m02c_rbac.up.sql');
|
||||
const rbacDownSql = read('database/migrations/2026061805_m02c_rbac.down.sql');
|
||||
const rbacVerifySql = read('database/migrations/2026061805_m02c_rbac.verify.sql');
|
||||
|
||||
const coreTables = [
|
||||
'qipai_schema_migrations',
|
||||
@@ -112,4 +115,14 @@ assert.match(authUpSql, /UNIQUE KEY uq_qipai_user_identities_tenant_app_openid/)
|
||||
assert.match(authUpSql, /revoked_at DATETIME\(3\)/);
|
||||
assert.match(authUpSql, /expires_at DATETIME\(3\)/);
|
||||
|
||||
console.log('PASS: M01-B through M02-B migration contracts are present.');
|
||||
for (const table of [
|
||||
'qipai_permissions', 'qipai_roles', 'qipai_role_permissions',
|
||||
'qipai_user_roles', 'qipai_user_store_scopes'
|
||||
]) {
|
||||
assert.match(rbacUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`));
|
||||
assert.match(rbacDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}\\b`));
|
||||
assert.match(rbacVerifySql, new RegExp(`'${table}'`));
|
||||
}
|
||||
assert.match(rbacUpSql, /PRIMARY KEY \(tenant_id, user_id, store_id, scope_type\)/);
|
||||
|
||||
console.log('PASS: M01-B through M02-C migration contracts are present.');
|
||||
|
||||
@@ -15,7 +15,8 @@ assert.equal(plan.direction, 'up');
|
||||
assert.match(plan.file, /2026061601_m01b_core_schema\.up\.sql/);
|
||||
assert.match(plan.file, /2026061802_m01c_async_tasks\.up\.sql/);
|
||||
assert.match(plan.file, /2026061803_m02a_tenant_apps\.up\.sql/);
|
||||
assert.match(plan.file, /2026061804_m02b_wechat_auth\.up\.sql$/);
|
||||
assert.match(plan.file, /2026061804_m02b_wechat_auth\.up\.sql/);
|
||||
assert.match(plan.file, /2026061805_m02c_rbac\.up\.sql$/);
|
||||
assert.match(plan.checksum, /^[a-f0-9]{64}$/);
|
||||
assert.ok(plan.statements.length >= 11);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PlatformConfigRepository
|
||||
} from '../dist/tenancy/platform-config-repository.js';
|
||||
import { AuthRepository } from '../dist/auth/auth-repository.js';
|
||||
import { RbacRepository } from '../dist/auth/rbac-repository.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -27,7 +28,10 @@ const expectedTables = [
|
||||
'qipai_orders',
|
||||
'qipai_outbox_events',
|
||||
'qipai_payments',
|
||||
'qipai_permissions',
|
||||
'qipai_platform_apps',
|
||||
'qipai_role_permissions',
|
||||
'qipai_roles',
|
||||
'qipai_rooms',
|
||||
'qipai_schema_migrations',
|
||||
'qipai_stores',
|
||||
@@ -35,6 +39,8 @@ const expectedTables = [
|
||||
'qipai_tenant_configs',
|
||||
'qipai_tenants',
|
||||
'qipai_user_identities',
|
||||
'qipai_user_roles',
|
||||
'qipai_user_store_scopes',
|
||||
'qipai_users'
|
||||
];
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
@@ -56,9 +62,9 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804']
|
||||
['2026061601', '2026061802', '2026061803', '2026061804', '2026061805']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -193,6 +199,28 @@ async function assertRevocableAuthSession(pool, context) {
|
||||
userAgent: 'M02-B test'
|
||||
});
|
||||
assert.equal(session.user.userType, 'CUSTOMER');
|
||||
const rbac = new RbacRepository(pool);
|
||||
assert.deepEqual(await rbac.getAccessProfile(context.tenantId, session.user.id), {
|
||||
roles: ['CUSTOMER'],
|
||||
capabilities: ['order.self.read', 'profile.read'],
|
||||
storeIds: []
|
||||
});
|
||||
const [storeResult] = await pool.query(
|
||||
`INSERT INTO qipai_stores (tenant_id, name) VALUES (?, 'M02C Store')`,
|
||||
[context.tenantId]
|
||||
);
|
||||
assert.equal(await rbac.grantStore({
|
||||
tenantId: context.tenantId,
|
||||
userId: session.user.id,
|
||||
storeId: String(storeResult.insertId),
|
||||
scopeType: 'STAFF'
|
||||
}), true);
|
||||
assert.equal(await rbac.grantStore({
|
||||
tenantId: String(Number(context.tenantId) + 1),
|
||||
userId: session.user.id,
|
||||
storeId: String(storeResult.insertId),
|
||||
scopeType: 'STAFF'
|
||||
}), false);
|
||||
assert.equal((await repository.loginWithWechat({
|
||||
context,
|
||||
openid: 'm02b-openid-a',
|
||||
@@ -249,7 +277,8 @@ try {
|
||||
{ version: '2026061601', name: 'm01b_core_schema' },
|
||||
{ version: '2026061802', name: 'm01c_async_tasks' },
|
||||
{ version: '2026061803', name: 'm02a_tenant_apps' },
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' }
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' },
|
||||
{ version: '2026061805', name: 'm02c_rbac' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
@@ -269,7 +298,8 @@ try {
|
||||
{ version: '2026061601', name: 'm01b_core_schema' },
|
||||
{ version: '2026061802', name: 'm01c_async_tasks' },
|
||||
{ version: '2026061803', name: 'm02a_tenant_apps' },
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' }
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' },
|
||||
{ version: '2026061805', name: 'm02c_rbac' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -300,7 +330,9 @@ try {
|
||||
'cross-tenant bootstrap rejection',
|
||||
'openid identity reuse',
|
||||
'session revocation',
|
||||
'role-version invalidation'
|
||||
'role-version invalidation',
|
||||
'customer capabilities',
|
||||
'cross-tenant store grant rejection'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { RbacRepository, roleCodes } from '../dist/auth/rbac-repository.js';
|
||||
|
||||
assert.deepEqual(roleCodes, [
|
||||
'CUSTOMER', 'CLEANER', 'STAFF', 'STORE_ADMIN', 'TENANT_ADMIN', 'PLATFORM_ADMIN'
|
||||
]);
|
||||
|
||||
const calls = [];
|
||||
const repository = new RbacRepository({
|
||||
async execute(sql, params) {
|
||||
calls.push([sql, params]);
|
||||
if (sql.includes('SELECT DISTINCT r.code')) return [[{ code: 'CUSTOMER' }], []];
|
||||
if (sql.includes('SELECT DISTINCT p.code')) {
|
||||
return [[{ code: 'order.self.read' }, { code: 'profile.read' }], []];
|
||||
}
|
||||
if (sql.includes('SELECT DISTINCT store_id')) return [[{ storeId: 11 }], []];
|
||||
return [{ affectedRows: 1 }, []];
|
||||
}
|
||||
});
|
||||
assert.deepEqual(await repository.getAccessProfile('7', '21'), {
|
||||
roles: ['CUSTOMER'],
|
||||
capabilities: ['order.self.read', 'profile.read'],
|
||||
storeIds: ['11']
|
||||
});
|
||||
assert.equal(await repository.grantStore({
|
||||
tenantId: '7', userId: '21', storeId: '11', scopeType: 'STAFF'
|
||||
}), true);
|
||||
assert.match(calls.at(-1)[0], /s\.tenant_id = \?/);
|
||||
assert.match(calls.at(-1)[0], /u\.tenant_id = \?/);
|
||||
|
||||
console.log('PASS: M02-C roles, capabilities and tenant-scoped store grants are present.');
|
||||
Reference in New Issue
Block a user