Files
neo/backend/migrations/tenant/20250128000001_add_authorization_system.js
Francisco Gaona 88f656c3f5 WIP - permissions
2025-12-28 05:43:03 +01:00

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');
});
};