102 lines
3.6 KiB
JavaScript
102 lines
3.6 KiB
JavaScript
/**
|
|
* 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');
|
|
});
|
|
};
|