WIP - field level permission

This commit is contained in:
Francisco Gaona
2025-12-30 05:54:56 +01:00
parent 56c0c3838d
commit d15fc918d1
8 changed files with 357 additions and 18 deletions

View File

@@ -583,7 +583,10 @@ export class ObjectService {
throw new NotFoundException('Record not found');
}
return record;
// Filter fields based on field-level permissions
const filteredRecord = await this.authService.filterReadableFields(record, objectDefModel.fields, user);
return filteredRecord;
}
async createRecord(
@@ -738,4 +741,57 @@ export class ObjectService {
return { success: true };
}
async getFieldPermissions(tenantId: string, objectId: string) {
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
// Get all field permissions for this object's fields
const permissions = await knex('role_field_permissions as rfp')
.join('field_definitions as fd', 'fd.id', 'rfp.fieldDefinitionId')
.where('fd.objectDefinitionId', objectId)
.select('rfp.*');
return permissions;
}
async updateFieldPermission(
tenantId: string,
roleId: string,
fieldDefinitionId: string,
canRead: boolean,
canEdit: boolean,
) {
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
// Check if permission already exists
const existing = await knex('role_field_permissions')
.where({ roleId, fieldDefinitionId })
.first();
if (existing) {
// Update existing permission
await knex('role_field_permissions')
.where({ roleId, fieldDefinitionId })
.update({
canRead,
canEdit,
updated_at: knex.fn.now(),
});
} else {
// Create new permission
await knex('role_field_permissions').insert({
id: knex.raw('(UUID())'),
roleId,
fieldDefinitionId,
canRead,
canEdit,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
}
return { success: true };
}
}

View File

@@ -3,6 +3,7 @@ import {
Get,
Post,
Patch,
Put,
Param,
Body,
UseGuards,
@@ -11,6 +12,7 @@ import { ObjectService } from './object.service';
import { FieldMapperService } from './field-mapper.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { TenantId } from '../tenant/tenant.decorator';
import { TenantDatabaseService } from '../tenant/tenant-database.service';
@Controller('setup/objects')
@UseGuards(JwtAuthGuard)
@@ -18,6 +20,7 @@ export class SetupObjectController {
constructor(
private objectService: ObjectService,
private fieldMapperService: FieldMapperService,
private tenantDbService: TenantDatabaseService,
) {}
@Get()
@@ -77,4 +80,21 @@ export class SetupObjectController {
) {
return this.objectService.updateObjectDefinition(tenantId, objectApiName, data);
}
@Get(':objectId/field-permissions')
async getFieldPermissions(
@TenantId() tenantId: string,
@Param('objectId') objectId: string,
) {
return this.objectService.getFieldPermissions(tenantId, objectId);
}
@Put(':objectId/field-permissions')
async updateFieldPermission(
@TenantId() tenantId: string,
@Param('objectId') objectId: string,
@Body() data: { roleId: string; fieldDefinitionId: string; canRead: boolean; canEdit: boolean },
) {
return this.objectService.updateFieldPermission(tenantId, data.roleId, data.fieldDefinitionId, data.canRead, data.canEdit);
}
}

View File

@@ -156,7 +156,20 @@ export class AbilityFactory {
return false;
}
// Check all roles for field permission
// Collect all field permissions from all roles
const allFieldPermissions: RoleFieldPermission[] = [];
for (const role of user.roles) {
if (role.fieldPermissions) {
allFieldPermissions.push(...role.fieldPermissions);
}
}
// If there are NO field permissions configured at all, allow by default
if (allFieldPermissions.length === 0) {
return true;
}
// If field permissions exist, check for explicit grants (union of all roles)
for (const role of user.roles) {
if (role.fieldPermissions) {
const fieldPerm = role.fieldPermissions.find(fp => fp.fieldDefinitionId === fieldDefinitionId);
@@ -167,8 +180,8 @@ export class AbilityFactory {
}
}
// Default: allow if no explicit restriction
return true;
// Field permissions exist but this field is not explicitly granted → deny
return false;
}
/**

View File

@@ -2,8 +2,12 @@ import { Module } from '@nestjs/common';
import { RbacService } from './rbac.service';
import { AbilityFactory } from './ability.factory';
import { AuthorizationService } from './authorization.service';
import { SetupRolesController } from './setup-roles.controller';
import { TenantModule } from '../tenant/tenant.module';
@Module({
imports: [TenantModule],
controllers: [SetupRolesController],
providers: [RbacService, AbilityFactory, AuthorizationService],
exports: [RbacService, AbilityFactory, AuthorizationService],
})

View File

@@ -0,0 +1,23 @@
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { TenantId } from '../tenant/tenant.decorator';
import { TenantDatabaseService } from '../tenant/tenant-database.service';
import { Role } from '../models/role.model';
@Controller('setup/roles')
@UseGuards(JwtAuthGuard)
export class SetupRolesController {
constructor(private tenantDbService: TenantDatabaseService) {}
@Get()
async getRoles(@TenantId() tenantId: string) {
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
return await Role.query(knex).select('*').orderBy('name', 'asc');
}
}