/** * Migration: Add authorization system (CASL + polymorphic sharing) * * This migration adds: * 1. Access control fields to object_definitions * 2. Field-level permissions to field_definitions * 3. role_rules table for CASL rules storage * 4. record_shares table for polymorphic per-record sharing */ exports.up = async function(knex) { // 1. Add access control fields to object_definitions await knex.schema.table('object_definitions', (table) => { table.enum('access_model', ['public', 'owner', 'mixed']).defaultTo('owner'); table.boolean('public_read').defaultTo(false); table.boolean('public_create').defaultTo(false); table.boolean('public_update').defaultTo(false); table.boolean('public_delete').defaultTo(false); table.string('owner_field', 100).defaultTo('ownerId'); }); // 2. Add field-level permission columns to field_definitions await knex.schema.table('field_definitions', (table) => { table.boolean('default_readable').defaultTo(true); table.boolean('default_writable').defaultTo(true); }); // 3. Create role_rules table for storing CASL rules per role await knex.schema.createTable('role_rules', (table) => { table.uuid('id').primary().defaultTo(knex.raw('(UUID())')); table.uuid('role_id').notNullable(); table.json('rules_json').notNullable(); // Array of CASL rules table.timestamps(true, true); // Foreign keys table.foreign('role_id') .references('id') .inTable('roles') .onDelete('CASCADE'); // Indexes table.index('role_id'); }); // 4. Create record_shares table for polymorphic per-record sharing await knex.schema.createTable('record_shares', (table) => { table.uuid('id').primary().defaultTo(knex.raw('(UUID())')); table.uuid('object_definition_id').notNullable(); table.string('record_id', 255).notNullable(); // String to support UUID/int uniformly table.uuid('grantee_user_id').notNullable(); table.uuid('granted_by_user_id').notNullable(); table.json('actions').notNullable(); // Array like ["read"], ["read","update"] table.json('fields').nullable(); // Optional field scoping table.timestamp('expires_at').nullable(); table.timestamp('revoked_at').nullable(); table.timestamp('created_at').defaultTo(knex.fn.now()); // Foreign keys table.foreign('object_definition_id') .references('id') .inTable('object_definitions') .onDelete('CASCADE'); table.foreign('grantee_user_id') .references('id') .inTable('users') .onDelete('CASCADE'); table.foreign('granted_by_user_id') .references('id') .inTable('users') .onDelete('CASCADE'); // Indexes for efficient querying table.index(['grantee_user_id', 'object_definition_id']); table.index(['object_definition_id', 'record_id']); table.unique(['object_definition_id', 'record_id', 'grantee_user_id']); }); }; exports.down = async function(knex) { // Drop tables in reverse order await knex.schema.dropTableIfExists('record_shares'); await knex.schema.dropTableIfExists('role_rules'); // Remove columns from field_definitions await knex.schema.table('field_definitions', (table) => { table.dropColumn('default_readable'); table.dropColumn('default_writable'); }); // Remove columns from object_definitions await knex.schema.table('object_definitions', (table) => { table.dropColumn('access_model'); table.dropColumn('public_read'); table.dropColumn('public_create'); table.dropColumn('public_update'); table.dropColumn('public_delete'); table.dropColumn('owner_field'); }); };