access-manager/lib/migration.js
TARS 0b0871ffea feat: Access Manager v3 — RBAC engine, SQLite, permission system
- SQLite database with full schema: users, roles, permissions,
  role_permissions, user_roles, services, audit_log
- RBAC engine with wildcard permission resolution (*.*.*)
- Automatic v2→v3 migration from JSON files
- 5 default roles: super_admin, admin, editor, user, viewer
- Feature registration for APP, GGL, FDX (119 permissions)
- 8 services seeded
- Full API: roles CRUD, permission check, user-role assignment,
  feature registration, audit log, stats
- Backward compatible with existing auth flows
2026-04-16 00:57:27 +00:00

238 lines
11 KiB
JavaScript

const db = require('./db');
const rbac = require('./rbac');
const fs = require('fs');
const path = require('path');
const bcrypt = require('bcryptjs');
const DATA_DIR = path.join(__dirname, '..', 'data');
const USERS_FILE = path.join(DATA_DIR, 'users.json');
function migrate() {
console.log('[migration] Starting v2 → v3 migration...');
// 1. Migrate users from JSON
migrateUsers();
// 2. Seed default roles
seedRoles();
// 3. Seed wildcard permissions
seedWildcardPermissions();
// 4. Assign roles to migrated users
assignMigratedUserRoles();
// 5. Register initial features for UseCaseGen apps
registerUseCaseGenFeatures();
// 6. Seed services
seedServices();
console.log('[migration] Migration complete.');
}
function migrateUsers() {
if (!fs.existsSync(USERS_FILE)) {
console.log('[migration] No users.json found — skipping user migration');
return;
}
const existing = db.prepare('SELECT COUNT(*) as c FROM users').get();
if (existing.c > 0) {
console.log(`[migration] Users table already has ${existing.c} rows — skipping`);
return;
}
const users = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
const insert = db.prepare(`
INSERT OR IGNORE INTO users (email, name, password_hash, auth_methods, metadata, tags, created_at, created_by, last_login, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
let count = 0;
for (const u of users) {
insert.run(
u.email, u.name, u.passwordHash || null,
JSON.stringify(u.authMethods || ['google']),
JSON.stringify(u.metadata || {}),
JSON.stringify(u.tags || []),
u.createdAt || new Date().toISOString(),
u.createdBy || 'system',
u.lastLogin || null,
'active'
);
count++;
}
console.log(`[migration] Migrated ${count} users from JSON`);
}
function seedRoles() {
const existing = db.prepare('SELECT COUNT(*) as c FROM roles').get();
if (existing.c > 0) {
console.log(`[migration] Roles already seeded (${existing.c}) — skipping`);
return;
}
const roles = [
['super_admin', 'Super Administrator', 'Full access to all apps and features', 1, 1],
['admin', 'Administrator', 'App-level admin with full feature access', 1, 10],
['editor', 'Editor', 'Can create, edit, and manage content', 1, 20],
['user', 'User', 'Standard user — chat and content reading', 1, 30],
['viewer', 'Viewer', 'Read-only access', 1, 40],
];
const insert = db.prepare('INSERT INTO roles (name, display_name, description, is_system, priority) VALUES (?, ?, ?, ?, ?)');
for (const r of roles) insert.run(...r);
console.log(`[migration] Seeded ${roles.length} default roles`);
}
function seedWildcardPermissions() {
const existing = db.prepare("SELECT COUNT(*) as c FROM permissions WHERE app = '*'").get();
if (existing.c > 0) return;
// Wildcard permission entries
const wildcards = [
['*', '*', '*', 'Full Access', 'Unrestricted access to everything', 'system'],
['*', '*', 'read', 'Global Read', 'Read access to all features', 'system'],
['*', '*', 'write', 'Global Write', 'Write access to all features', 'system'],
['*', '*', 'delete', 'Global Delete', 'Delete access to all features', 'system'],
['*', '*', 'admin', 'Global Admin', 'Admin access to all features', 'system'],
['*', 'chat', 'read', 'Chat Read (all apps)', 'Read chat in all apps', 'chat'],
['*', 'chat', 'write', 'Chat Write (all apps)', 'Write chat in all apps', 'chat'],
['*', 'content', 'read', 'Content Read (all apps)', 'Read content in all apps', 'content'],
['*', 'content', 'write', 'Content Write (all apps)', 'Write content in all apps', 'content'],
['*', 'content', 'delete', 'Content Delete (all apps)', 'Delete content in all apps', 'content'],
];
const insert = db.prepare('INSERT OR IGNORE INTO permissions (app, feature, action, display_name, description, category) VALUES (?, ?, ?, ?, ?, ?)');
for (const w of wildcards) insert.run(...w);
// Assign permissions to roles
const superAdmin = db.prepare("SELECT id FROM roles WHERE name = 'super_admin'").get();
const admin = db.prepare("SELECT id FROM roles WHERE name = 'admin'").get();
const editor = db.prepare("SELECT id FROM roles WHERE name = 'editor'").get();
const user = db.prepare("SELECT id FROM roles WHERE name = 'user'").get();
const viewer = db.prepare("SELECT id FROM roles WHERE name = 'viewer'").get();
const perm = (app, feat, act) => db.prepare('SELECT id FROM permissions WHERE app=? AND feature=? AND action=?').get(app, feat, act);
const assign = db.prepare('INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)');
// super_admin: *.*.*
assign.run(superAdmin.id, perm('*', '*', '*').id);
// admin: *.*.read, *.*.write, *.*.delete, *.*.admin
assign.run(admin.id, perm('*', '*', 'read').id);
assign.run(admin.id, perm('*', '*', 'write').id);
assign.run(admin.id, perm('*', '*', 'delete').id);
assign.run(admin.id, perm('*', '*', 'admin').id);
// editor: *.content.*, *.chat.*
assign.run(editor.id, perm('*', 'content', 'read').id);
assign.run(editor.id, perm('*', 'content', 'write').id);
assign.run(editor.id, perm('*', 'content', 'delete').id);
assign.run(editor.id, perm('*', 'chat', 'read').id);
assign.run(editor.id, perm('*', 'chat', 'write').id);
// user: *.chat.read, *.chat.write, *.content.read
assign.run(user.id, perm('*', 'chat', 'read').id);
assign.run(user.id, perm('*', 'chat', 'write').id);
assign.run(user.id, perm('*', 'content', 'read').id);
// viewer: *.*.read
assign.run(viewer.id, perm('*', '*', 'read').id);
console.log('[migration] Seeded wildcard permissions and role assignments');
}
function assignMigratedUserRoles() {
const existingAssignments = db.prepare('SELECT COUNT(*) as c FROM user_roles').get();
if (existingAssignments.c > 0) {
console.log(`[migration] User-role assignments already exist (${existingAssignments.c}) — skipping`);
return;
}
// Read original users.json to get their v2 role + services
if (!fs.existsSync(USERS_FILE)) return;
const v2Users = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
for (const v2 of v2Users) {
const user = db.prepare('SELECT id FROM users WHERE email = ? COLLATE NOCASE').get(v2.email);
if (!user) continue;
if (v2.role === 'admin' && v2.services && v2.services.includes('*')) {
// Admin with wildcard → super_admin
rbac.assignRole(v2.email, 'super_admin', '*', 'migration');
console.log(`[migration] ${v2.email} → super_admin (global)`);
} else if (v2.role === 'admin') {
// Admin with specific services → admin scoped
for (const svc of (v2.services || [])) {
rbac.assignRole(v2.email, 'admin', svc, 'migration');
}
console.log(`[migration] ${v2.email} → admin (scoped: ${(v2.services || []).join(', ')})`);
} else {
// Everyone else → user
rbac.assignRole(v2.email, 'user', '*', 'migration');
console.log(`[migration] ${v2.email} → user (global)`);
}
}
}
function registerUseCaseGenFeatures() {
const existingPerms = db.prepare("SELECT COUNT(*) as c FROM permissions WHERE app = 'usecasegen'").get();
if (existingPerms.c > 0) return;
const commonFeatures = [
{ feature: 'chat', actions: ['read', 'write'], category: 'chat', display_name: 'Chat / Sessions' },
{ feature: 'manage.focus_areas', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Focus Areas' },
{ feature: 'manage.tech_patterns', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Tech Patterns' },
{ feature: 'manage.regions', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Regions' },
{ feature: 'manage.audience', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Audience' },
{ feature: 'manage.analysis_focus', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Analysis Focus' },
{ feature: 'sessions', actions: ['read', 'write', 'delete'], category: 'content', display_name: 'Saved Sessions' },
{ feature: 'admin_panel', actions: ['read', 'admin'], category: 'admin', display_name: 'Admin Panel' },
{ feature: 'user_management', actions: ['read', 'write', 'admin'], category: 'admin', display_name: 'User Management' },
{ feature: 'settings', actions: ['read', 'write'], category: 'settings', display_name: 'App Settings' },
];
rbac.registerFeatures('usecasegen', commonFeatures);
rbac.registerFeatures('ggl-usecasegen', commonFeatures);
const fdxFeatures = [
{ feature: 'chat', actions: ['read', 'write'], category: 'chat', display_name: 'Chat / Sessions' },
{ feature: 'manage.patterns', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Patterns' },
{ feature: 'manage.regions', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Regions' },
{ feature: 'manage.audience', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Audience' },
{ feature: 'manage.analysis', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Analysis Focus' },
{ feature: 'sessions', actions: ['read', 'write', 'delete'], category: 'content', display_name: 'Saved Sessions' },
{ feature: 'enterprise_systems', actions: ['read', 'write', 'delete'], category: 'content', display_name: 'Enterprise Systems' },
{ feature: 'admin_panel', actions: ['read', 'admin'], category: 'admin', display_name: 'Admin Panel' },
{ feature: 'user_management', actions: ['read', 'write', 'admin'], category: 'admin', display_name: 'User Management' },
{ feature: 'settings', actions: ['read', 'write'], category: 'settings', display_name: 'App Settings' },
];
rbac.registerFeatures('fedex-cohort-app', fdxFeatures);
console.log('[migration] Registered UseCaseGen features for all three apps');
}
function seedServices() {
const existing = db.prepare('SELECT COUNT(*) as c FROM services').get();
if (existing.c > 0) return;
const services = [
['usecasegen', 'UseCaseGen APP', 'app.usecasegen.app', '["app.usecasegen.app","usecasegen.app","www.usecasegen.app"]', 3006],
['ggl-usecasegen', 'GGL UseCaseGen', 'ggl.usecasegen.app', '["ggl.usecasegen.app"]', 3095],
['fedex-cohort-app', 'FDX Trade Intel', 'fdx.usecasegen.app', '["fdx.usecasegen.app"]', 3089],
['taos', 'GoalStack / TAOS', 'goalstack.scottfelten.com', '["goalstack.scottfelten.com","taos.scottfelten.com"]', null],
['access-manager', 'Access Manager', 'users.scottfelten.com', '["users.scottfelten.com"]', 3030],
['intellicert', 'IntelliCert', 'intellicert.app', '["intellicert.app","www.intellicert.app"]', null],
['customer-360', 'Customer 360', 'c360.scottfelten.com', '["c360.scottfelten.com"]', null],
['command-center', 'Docs / Command Center', 'docs.scottfelten.com', '["docs.scottfelten.com"]', null],
];
const insert = db.prepare('INSERT OR IGNORE INTO services (id, display_name, hostname, hostnames, port) VALUES (?, ?, ?, ?, ?)');
for (const s of services) insert.run(...s);
console.log(`[migration] Seeded ${services.length} services`);
}
module.exports = { migrate };