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 @@ + + + diff --git a/frontend/components/ObjectAccessSettings.vue b/frontend/components/ObjectAccessSettings.vue index 0df7aed..afd1af4 100644 --- a/frontend/components/ObjectAccessSettings.vue +++ b/frontend/components/ObjectAccessSettings.vue @@ -43,20 +43,19 @@ - - - Field-Level Security - - Control field visibility and editability by role (coming soon) - - - -
- Field-level permissions will be managed through role configuration. - Navigate to Setup → Roles to configure field access for each role. -
-
-
+ + +
+ Object ID not available +
+ +
+ No fields available +
@@ -65,10 +64,13 @@ import { ref, watch } from 'vue'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'; import { Label } from '~/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select'; +import FieldLevelSecurity from '~/components/FieldLevelSecurity.vue'; const props = defineProps<{ objectApiName: string; + objectId?: string; orgWideDefault?: string; + fields?: any[]; }>(); const emit = defineEmits<{ diff --git a/frontend/pages/setup/objects/[apiName].vue b/frontend/pages/setup/objects/[apiName].vue index e53862f..8304bdf 100644 --- a/frontend/pages/setup/objects/[apiName].vue +++ b/frontend/pages/setup/objects/[apiName].vue @@ -60,7 +60,9 @@