Skip to content

Security Layers Documentation: SensitiveRelationsInterceptor & Permission‐Based Security

samuel mbabhazi edited this page Jul 8, 2025 · 1 revision

Overview

Ever-Gauzy implements a robust security system to protect sensitive data and control access to entity relations. This documentation covers the current implementation, focusing on the SensitiveRelationsInterceptor for relation-based authorization and @Permissions decorators for method-level access control.

Table of Contents

  1. Security Architecture
  2. SensitiveRelationsInterceptor (Authorization Layer)
  3. Permission-Based Security
  4. Configuration Guide
  5. Implementation Examples
  6. Best Practices
  7. Troubleshooting
  8. Advanced Configuration
  9. Security Considerations
  10. Migration Guide

Security Architecture

The Ever-Gauzy security system employs a layered security approach with the following components:

graph TD
    A[Client Request] --> B[Permission Guards]
    B --> C{Has Permission?}
    C -->|No| D[403 Forbidden]
    C -->|Yes| E[SensitiveRelationsInterceptor]
    E --> F{Relation Access Allowed?}
    F -->|No| G[403 Forbidden]
    F -->|Yes| H[Controller Action]
    H --> I[Database Query]
    I --> J[Entity Response with @Exclude filtering]
    J --> K[Client]
Loading

Layer 1: Permission Guards (@Permissions)

  • Purpose: Method-level access control
  • Scope: Controller actions and endpoints
  • Action: Validates user permissions before method execution

Layer 2: SensitiveRelationsInterceptor

  • Purpose: Relation-specific authorization
  • Scope: Entity relations access control
  • Action: Blocks unauthorized relation access before database queries

Layer 3: Entity-Level Field Protection (@Exclude)

  • Purpose: Field-level data protection
  • Scope: Sensitive entity fields
  • Action: Excludes sensitive fields from serialization

SensitiveRelationsInterceptor (Authorization Layer)

Purpose and Functionality

The SensitiveRelationsInterceptor acts as a gatekeeper that validates user permissions before allowing access to sensitive entity relations. It prevents unauthorized users from even attempting to load protected data.

Key Features

  • Pre-execution validation: Checks permissions before database queries
  • Relation-based protection: Controls access to specific entity relationships
  • Nested relation support: Handles complex relation paths (e.g., organization.employees.user)
  • Configurable permissions: Maps relations to required permissions
  • Request blocking: Returns 403 Forbidden for unauthorized access

Implementation Details

Core Components

File: packages/core/src/lib/core/interceptors/sensitive-relations.interceptor.ts

@Injectable()
export class SensitiveRelationsInterceptor implements NestInterceptor {
    constructor(private readonly reflector: Reflector) {}

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        // Extract configuration from decorators
        const config: SensitiveRelationConfig =
            this.reflector.get(SENSITIVE_RELATIONS_KEY, handler) ||
            this.reflector.get(SENSITIVE_RELATIONS_KEY, controller);

        // Extract requested relations from query/body
        const request = context.switchToHttp().getRequest();
        let relations = request.query?.relations || request.body?.relations || [];

        // Validate each requested relation
        for (const rel of validRelations) {
            const requiredPermission = getRequiredPermissionForRelation(configToUse, rel);
            if (requiredPermission && !RequestContext.hasPermission(requiredPermission)) {
                throw new ForbiddenException(
                    `Access to the sensitive relation '${rel}' is forbidden. Required permission: '${requiredPermission}'.`
                );
            }
        }

        return next.handle();
    }
}

Configuration Structure

File: packages/core/src/lib/core/decorators/sensitive-relations.decorator.ts

export interface SensitiveRelationConfig {
    [relation: string]: PermissionsEnum | SensitiveRelationConfig | null;
}

export const SensitiveRelations = (config: SensitiveRelationConfig, rootKey?: string) => {
    const decorators = [SetMetadata(SENSITIVE_RELATIONS_KEY, config)];
    if (rootKey) {
        decorators.push(SetMetadata(SENSITIVE_RELATIONS_ROOT_KEY, rootKey));
    }
    return (target: any, key?: any, descriptor?: any) => {
        decorators.forEach((decorator) => decorator(target, key, descriptor));
    };
};

Usage Example

@ApiTags('Employee')
@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT)
@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS, 'organization')
@Controller('/employee')
export class EmployeeController extends CrudController<Employee> {
    // Controller methods...
}

Permission-Based Security

Purpose and Functionality

Ever-Gauzy uses @Permissions decorators combined with Permission Guards to provide method-level access control. This system ensures that only users with appropriate permissions can access specific controller actions.

Key Features

  • Declarative permissions: Uses @Permissions() decorators on controller methods
  • Guard-based validation: Leverages PermissionGuard and TenantPermissionGuard
  • Pre-execution validation: Checks permissions before method execution
  • Flexible permission combinations: Supports multiple permissions per method

Implementation Details

Permission Guards

Files:

  • packages/core/src/lib/shared/guards/permission.guard.ts
  • packages/core/src/lib/shared/guards/tenant-permission.guard.ts

Permission Decorator

File: packages/core/src/lib/shared/decorators/permissions.decorator.ts

@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT)
@Controller('/employee')
export class EmployeeController {
    // Methods protected by permission guards
}

Entity-Level Field Protection

Ever-Gauzy uses @Exclude decorators to protect sensitive fields at the entity level, ensuring they are not included in API responses.

User Entity Sensitive Fields

@MultiORMEntity('user')
export class User extends TenantBaseEntity implements IUser {
    // Public field - always visible
    @MultiORMColumn({ nullable: true })
    firstName?: string;

    // Sensitive field - excluded from serialization
    @Exclude({ toPlainOnly: true })
    @MultiORMColumn({ nullable: true })
    hash?: string;

    @Exclude({ toPlainOnly: true })
    @MultiORMColumn({ insert: false, nullable: true })
    refreshToken?: string;

    @Exclude({ toPlainOnly: true })
    @MultiORMColumn({ insert: false, nullable: true })
    code?: string;
}

Invite Entity Protection

@MultiORMEntity('invite')
export class Invite extends TenantOrganizationBaseEntity implements IInvite {
    // Sensitive invite code - excluded from responses
    @Exclude({ toPlainOnly: true })
    @MultiORMColumn({ nullable: true })
    code?: string;
}

Image Asset Entity Protection

@MultiORMEntity('image_asset')
export class ImageAsset extends TenantOrganizationBaseEntity implements IImageAsset {
    // Sensitive provider information - excluded from responses
    @Exclude({ toPlainOnly: true })
    @MultiORMColumn({ nullable: true })
    externalProviderId?: string;

    @Exclude({ toPlainOnly: true })
    @MultiORMColumn({ type: 'simple-enum', nullable: true, enum: FileStorageProviderEnum })
    storageProvider?: FileStorageProvider;
}

Configuration Guide

Setting Up SensitiveRelationsInterceptor

1. Define Sensitive Relations Configuration

File: packages/core/src/lib/core/util/organization-sensitive-relations.config.ts

const sharedOrganizationRelations = {
    payments: PermissionsEnum.ORG_PAYMENT_VIEW,
    invoices: PermissionsEnum.ALL_ORG_VIEW,
    invoiceEstimateHistories: PermissionsEnum.ALL_ORG_VIEW,
    accountingTemplates: PermissionsEnum.VIEW_ALL_ACCOUNTING_TEMPLATES,
    employees: {
        _self: PermissionsEnum.ORG_EMPLOYEES_VIEW,
        user: PermissionsEnum.ORG_USERS_VIEW
    },
    featureOrganizations: PermissionsEnum.ALL_ORG_VIEW,
    contact: PermissionsEnum.ORG_CONTACT_VIEW,
    organizationSprints: PermissionsEnum.ORG_SPRINT_VIEW
};

export const ORGANIZATION_SENSITIVE_RELATIONS: SensitiveRelationConfig = {
    ...sharedOrganizationRelations,
    organization: {
        _self: null, // Organization itself doesn't require special permission
        ...sharedOrganizationRelations
    }
};

2. Apply to Controllers

@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS)
@Controller('/organization')
export class OrganizationController {
    // Methods automatically protected
}

3. Configure Nested Relations

For nested relations like organization.employees.user:

const config = {
    organization: {
        employees: {
            _self: PermissionsEnum.ORG_EMPLOYEES_VIEW,
            user: PermissionsEnum.ORG_USERS_VIEW
        }
    }
};

Setting Up Permission-Based Security

1. Apply Permission Guards to Controllers

@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ORG_USERS_VIEW)
@Controller('/users')
export class UserController {
    // All methods protected by permission guards

    @Permissions(PermissionsEnum.ORG_USERS_EDIT)
    @Post('/')
    async create(@Body() entity: CreateUserDTO): Promise<IUser> {
        // Only users with ORG_USERS_EDIT permission can access
    }
}

2. Mark Sensitive Fields in Entities

export class Employee extends TenantOrganizationBaseEntity {
    // Always visible
    @MultiORMColumn()
    firstName?: string;

    // Always hidden from API responses
    @Exclude({ toPlainOnly: true })
    @MultiORMColumn()
    socialSecurityNumber?: string;

    // Sensitive financial information - always excluded
    @Exclude({ toPlainOnly: true })
    @MultiORMColumn({ type: 'numeric' })
    salary?: number;
}

Implementation Examples

Complete Controller Setup

@ApiTags('Employee')
@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT)
@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS, 'organization')
@Controller('/employee')
export class EmployeeController extends CrudController<Employee> {

    @Permissions(PermissionsEnum.ORG_EMPLOYEES_VIEW)
    @Get(':id')
    async findById(
        @Param('id', UUIDValidationPipe) id: ID,
        @Query() options: EmployeeFindOptionsQueryDTO
    ): Promise<IEmployee> {
        // Security layers applied:
        // 1. Permission Guards validate user permissions
        // 2. SensitiveRelationsInterceptor validates relation access
        // 3. @Exclude decorators filter sensitive fields
        return await this.employeeService.findOneByIdString(id, options);
    }
}

Request Flow Example

  1. Client Request: GET /employee/123?relations=organization.employees,organization.payments

  2. Permission Guards:

    • Validates user has ORG_EMPLOYEES_VIEW permission
    • Throws 403 if permission missing
  3. SensitiveRelationsInterceptor:

    • Checks if user has ORG_EMPLOYEES_VIEW for organization.employees relation
    • Checks if user has ORG_PAYMENT_VIEW for organization.payments relation
    • Throws 403 if relation permissions missing
  4. Controller Action: Executes if all permissions valid

  5. Entity Serialization:

    • Applies @Exclude decorators to filter sensitive fields
    • Removes fields like hash, refreshToken, socialSecurityNumber
  6. Client Response: Filtered, authorized data

Best Practices

1. Layered Security Approach

  • Always use both layers for comprehensive protection
  • SensitiveRelationsInterceptor for access control
  • PermissionBasedSerializer for data filtering

2. Configuration Management

  • Centralize configurations in dedicated files
  • Use shared configurations for consistency
  • Document permission requirements clearly

3. Permission Granularity

  • Define specific permissions for different relation types
  • Use nested configurations for complex relationships
  • Consider _self permissions for entity access

4. Entity Design

  • Mark sensitive fields with appropriate @Exclude decorators
  • Use role-based groups for conditional exclusion
  • Document field sensitivity levels

5. Testing

  • Test permission scenarios thoroughly
  • Verify both authorized and unauthorized access
  • Check field filtering for different roles

Troubleshooting

Common Issues

1. 403 Forbidden Errors

Problem: Users getting blocked from accessing relations

Solutions:

  • Verify user has required permissions
  • Check sensitive relations configuration
  • Ensure permissions are properly assigned to roles

2. Sensitive Data Exposure

Problem: Sensitive fields appearing in responses

Solutions:

  • Add @Exclude({ toPlainOnly: true }) to sensitive entity fields
  • Verify entity fields are properly marked with exclusion decorators
  • Check that sensitive data is not being manually included in responses

3. Configuration Not Applied

Problem: Security measures not working as expected

Solutions:

  • Ensure @UseGuards() and @Permissions() decorators are present
  • Verify @UseInterceptors(SensitiveRelationsInterceptor) is applied
  • Check decorator order and placement
  • Ensure permission guards are properly imported

Debugging Tips

  1. Enable logging in interceptors for troubleshooting
  2. Test with different user roles to verify filtering
  3. Use API testing tools to validate responses
  4. Check permission assignments in database

Related Resources

Advanced Configuration

Custom Sensitive Relations Configuration

For specific use cases, you can create custom configurations:

// Custom configuration for project-specific relations
export const PROJECT_SENSITIVE_RELATIONS: SensitiveRelationConfig = {
    tasks: PermissionsEnum.ORG_TASK_VIEW,
    timeEntries: PermissionsEnum.ORG_TIME_TRACKING_VIEW,
    expenses: PermissionsEnum.ORG_EXPENSES_VIEW,
    team: {
        _self: PermissionsEnum.ORG_TEAM_VIEW,
        members: PermissionsEnum.ORG_TEAM_EDIT
    }
};

// Apply to specific controller
@SensitiveRelations(PROJECT_SENSITIVE_RELATIONS)
@Controller('/project')
export class ProjectController {
    // Project-specific protection
}

Advanced Permission Configurations

Complex permission setups for different scenarios:

// Multiple permissions - user needs ANY of these permissions
@Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT, PermissionsEnum.PROFILE_EDIT)
@Put('/:id')
async update(@Param('id') id: ID, @Body() entity: UpdateUserDTO): Promise<IUser> {
    // User can access if they have either ORG_EMPLOYEES_EDIT OR PROFILE_EDIT
}

// Method-specific permissions override controller-level permissions
@Controller('/employee')
@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ORG_EMPLOYEES_VIEW) // Default permission
export class EmployeeController {

    @Get('/')
    async findAll() {
        // Uses controller-level ORG_EMPLOYEES_VIEW permission
    }

    @Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT) // Override for this method
    @Post('/')
    async create(@Body() entity: CreateEmployeeDTO) {
        // Requires ORG_EMPLOYEES_EDIT permission specifically
    }
}

Dynamic Permission Checking

For complex scenarios requiring dynamic permission validation:

// Custom permission validation function
function getRequiredPermissionForRelation(
    config: SensitiveRelationConfig,
    relationPath: string
): PermissionsEnum | null {
    const pathParts = relationPath.split('.');
    let current: SensitiveRelationConfig | PermissionsEnum | null = config;

    for (const part of pathParts) {
        if (!current || typeof current !== 'object') return null;
        const value = current[part];

        if (typeof value === 'object' && value !== null) {
            if ('_self' in value && value._self) {
                return value._self as PermissionsEnum;
            }
            current = value as SensitiveRelationConfig;
        } else if (isValidPermission(value)) {
            return value as PermissionsEnum;
        } else {
            return null;
        }
    }
    return null;
}

Security Considerations

1. Defense in Depth

The two-layer approach provides multiple security checkpoints:

  • Layer 1 (Authorization): Prevents unauthorized data access attempts
  • Layer 2 (Serialization): Ensures sensitive data never leaves the server

2. Performance Implications

  • SensitiveRelationsInterceptor: Minimal overhead, runs before database queries
  • PermissionBasedSerializer: Processes after queries, consider caching for large datasets

3. Audit and Monitoring

Implement logging for security events:

@Injectable()
export class AuditingSensitiveRelationsInterceptor extends SensitiveRelationsInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest();
        const user = RequestContext.currentUser();

        // Log access attempts
        this.logger.log(`User ${user.id} accessing relations: ${request.query?.relations}`);

        return super.intercept(context, next).pipe(
            catchError((error) => {
                if (error instanceof ForbiddenException) {
                    this.logger.warn(`Unauthorized access attempt by user ${user.id}`);
                }
                throw error;
            })
        );
    }
}

Migration Guide

Upgrading from Previous Versions

If migrating from older security implementations:

  1. Replace manual permission checks with interceptor-based approach
  2. Update entity decorators to use @Exclude instead of manual filtering
  3. Centralize configurations in dedicated files
  4. Test thoroughly with different user roles and permissions

Backward Compatibility

The new security layers are designed to be backward compatible:

  • Existing controllers without interceptors continue to work
  • Manual permission checks can coexist with interceptors
  • Gradual migration is supported

Quick Reference

Essential Decorators and Imports

// Required imports
import { UseGuards, UseInterceptors } from '@nestjs/common';
import { SensitiveRelations } from '../core/decorators/sensitive-relations.decorator';
import { SensitiveRelationsInterceptor } from '../core/interceptors/sensitive-relations.interceptor';
import { ORGANIZATION_SENSITIVE_RELATIONS } from '../core/util/organization-sensitive-relations.config';
import { Permissions } from '../shared/decorators/permissions.decorator';
import { PermissionGuard, TenantPermissionGuard } from '../shared/guards';
import { Exclude } from 'class-transformer';
import { PermissionsEnum } from '@gauzy/contracts';

// Controller setup
@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.YOUR_PERMISSION)
@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS, 'organization')
@Controller('/your-endpoint')
export class YourController {
    // Your methods here
}

// Entity field protection
@Exclude({ toPlainOnly: true })
@MultiORMColumn()
sensitiveField?: string;

Common Permission Mappings

Relation Required Permission
employees ORG_EMPLOYEES_VIEW
employees.user ORG_USERS_VIEW
payments ORG_PAYMENT_VIEW
invoices ALL_ORG_VIEW
invoiceEstimateHistories ALL_ORG_VIEW
accountingTemplates VIEW_ALL_ACCOUNTING_TEMPLATES
featureOrganizations ALL_ORG_VIEW
contact ORG_CONTACT_VIEW
organizationSprints ORG_SPRINT_VIEW

Real Controller Examples

Employee Controller

@ApiTags('Employee')
@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT)
@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS, 'organization')
@Controller('/employee')
export class EmployeeController extends CrudController<Employee> {
    // Real implementation from the codebase
}

Organization Controller

@ApiTags('Organization')
@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ALL_ORG_EDIT)
@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS)
@Controller('/organization')
export class OrganizationController extends CrudController<Organization> {
    // Real implementation from the codebase
}

Organization Project Controller

@ApiTags('OrganizationProject')
@UseGuards(TenantPermissionGuard, ProjectManagerOrPermissionGuard)
@Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_PROJECT_EDIT)
@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS, 'organization')
@Controller('/organization-projects')
export class OrganizationProjectController extends CrudController<OrganizationProject> {
    // Real implementation from the codebase
}

Security Checklist

  • Permission Guards (TenantPermissionGuard, PermissionGuard) applied to controllers
  • @Permissions decorators configured on controller methods
  • SensitiveRelationsInterceptor applied to controllers with relation access
  • @SensitiveRelations decorator configured with appropriate permissions
  • @Exclude decorators on sensitive entity fields
  • Unit tests for security scenarios
  • Integration tests for different user permissions
  • Audit logging implemented
  • Performance monitoring in place

Summary

The Ever-Gauzy security system provides robust, multi-layered protection for sensitive data through:

🛡️ Three-Layer Defense

  1. Permission Guards: Method-level access control using @Permissions decorators
  2. SensitiveRelationsInterceptor: Relation-specific authorization for entity access
  3. Entity Field Protection: Field-level data filtering using @Exclude decorators

🔧 Key Benefits

  • Declarative Configuration: Simple decorator-based setup
  • Granular Control: Method, relation, and field-level permissions
  • Performance Optimized: Minimal overhead with efficient permission checking
  • Audit Ready: Built-in logging and monitoring capabilities
  • Developer Friendly: Clear error messages and debugging support

📋 Implementation Steps

  1. Apply permission guards to controllers using @UseGuards()
  2. Configure method permissions with @Permissions() decorators
  3. Apply SensitiveRelationsInterceptor for relation protection
  4. Configure sensitive relations with @SensitiveRelations()
  5. Mark sensitive entity fields with @Exclude() decorators
  6. Test with different user permissions and scenarios
  7. Monitor and audit security events

🚀 Best Practices

  • Always use permission guards as the primary security layer
  • Apply relation interceptors for controllers that expose entity relations
  • Mark all sensitive fields with @Exclude decorators
  • Centralize permission configurations in dedicated files
  • Implement comprehensive testing strategies
  • Monitor security events and metrics
  • Follow the principle of least privilege

This security architecture ensures that sensitive data is protected at multiple levels, providing defense-in-depth while maintaining developer productivity and system performance.

Important Notes

Current Implementation Status

✅ Currently Implemented:

  • @Permissions decorators with Permission Guards for method-level access control
  • SensitiveRelationsInterceptor for relation-based authorization
  • @SensitiveRelations decorator with ORGANIZATION_SENSITIVE_RELATIONS configuration
  • @Exclude decorators for entity field protection

❌ Not Currently Implemented:

  • PermissionBasedSerializer/SerializerInterceptor is not widely used in controllers
  • Role-based field filtering with @Expose groups is not actively implemented
  • Dynamic role-based serialization is not part of the current security model

Security Model Summary

Ever-Gauzy uses a three-layer security approach:

  1. Permission Guards (@Permissions + Guards) - Primary access control
  2. Relation Protection (@SensitiveRelations + Interceptor) - Relation-specific authorization
  3. Field Exclusion (@Exclude decorators) - Sensitive field protection

This model provides robust security without the complexity of role-based serialization, focusing on clear permission-based access control and field-level protection.

Real-World Usage

The security system is actively used in controllers like:

  • EmployeeController - Protects employee data and organization relations
  • OrganizationController - Controls access to organization data and relations
  • OrganizationProjectController - Manages project access with custom guards
  • UserOrganizationController - Protects user-organization relationships
Clone this wiki locally