diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts
index bbbb9e9..1c95597 100644
--- a/backend/src/object/object.service.ts
+++ b/backend/src/object/object.service.ts
@@ -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 };
+ }
}
diff --git a/backend/src/object/setup-object.controller.ts b/backend/src/object/setup-object.controller.ts
index ebb4713..e090769 100644
--- a/backend/src/object/setup-object.controller.ts
+++ b/backend/src/object/setup-object.controller.ts
@@ -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);
+ }
}
diff --git a/backend/src/rbac/ability.factory.ts b/backend/src/rbac/ability.factory.ts
index 67178e5..4e1fc28 100644
--- a/backend/src/rbac/ability.factory.ts
+++ b/backend/src/rbac/ability.factory.ts
@@ -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;
}
/**
diff --git a/backend/src/rbac/rbac.module.ts b/backend/src/rbac/rbac.module.ts
index c648404..b001756 100644
--- a/backend/src/rbac/rbac.module.ts
+++ b/backend/src/rbac/rbac.module.ts
@@ -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],
})
diff --git a/backend/src/rbac/setup-roles.controller.ts b/backend/src/rbac/setup-roles.controller.ts
new file mode 100644
index 0000000..98465bd
--- /dev/null
+++ b/backend/src/rbac/setup-roles.controller.ts
@@ -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');
+ }
+}
diff --git a/frontend/components/FieldLevelSecurity.vue b/frontend/components/FieldLevelSecurity.vue
new file mode 100644
index 0000000..5f2f3c2
--- /dev/null
+++ b/frontend/components/FieldLevelSecurity.vue
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+ Field
+
+ {{ role.name }}
+
+
+
+
+
+
+
+ Read
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+