-
Notifications
You must be signed in to change notification settings - Fork 641
Security Layers Documentation: SensitiveRelationsInterceptor & Permission‐Based Security
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.
- Security Architecture
- SensitiveRelationsInterceptor (Authorization Layer)
- Permission-Based Security
- Configuration Guide
- Implementation Examples
- Best Practices
- Troubleshooting
- Advanced Configuration
- Security Considerations
- Migration Guide
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]
- Purpose: Method-level access control
- Scope: Controller actions and endpoints
- Action: Validates user permissions before method execution
- Purpose: Relation-specific authorization
- Scope: Entity relations access control
- Action: Blocks unauthorized relation access before database queries
- Purpose: Field-level data protection
- Scope: Sensitive entity fields
- Action: Excludes sensitive fields from serialization
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.
- 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
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();
}
}
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));
};
};
@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...
}
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.
-
Declarative permissions: Uses
@Permissions()
decorators on controller methods -
Guard-based validation: Leverages
PermissionGuard
andTenantPermissionGuard
- Pre-execution validation: Checks permissions before method execution
- Flexible permission combinations: Supports multiple permissions per method
Files:
packages/core/src/lib/shared/guards/permission.guard.ts
packages/core/src/lib/shared/guards/tenant-permission.guard.ts
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
}
Ever-Gauzy uses @Exclude decorators to protect sensitive fields at the entity level, ensuring they are not included in API responses.
@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;
}
@MultiORMEntity('invite')
export class Invite extends TenantOrganizationBaseEntity implements IInvite {
// Sensitive invite code - excluded from responses
@Exclude({ toPlainOnly: true })
@MultiORMColumn({ nullable: true })
code?: string;
}
@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;
}
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
}
};
@UseInterceptors(SensitiveRelationsInterceptor)
@SensitiveRelations(ORGANIZATION_SENSITIVE_RELATIONS)
@Controller('/organization')
export class OrganizationController {
// Methods automatically protected
}
For nested relations like organization.employees.user
:
const config = {
organization: {
employees: {
_self: PermissionsEnum.ORG_EMPLOYEES_VIEW,
user: PermissionsEnum.ORG_USERS_VIEW
}
}
};
@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
}
}
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;
}
@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);
}
}
-
Client Request:
GET /employee/123?relations=organization.employees,organization.payments
-
Permission Guards:
- Validates user has
ORG_EMPLOYEES_VIEW
permission - Throws 403 if permission missing
- Validates user has
-
SensitiveRelationsInterceptor:
- Checks if user has
ORG_EMPLOYEES_VIEW
fororganization.employees
relation - Checks if user has
ORG_PAYMENT_VIEW
fororganization.payments
relation - Throws 403 if relation permissions missing
- Checks if user has
-
Controller Action: Executes if all permissions valid
-
Entity Serialization:
- Applies
@Exclude
decorators to filter sensitive fields - Removes fields like
hash
,refreshToken
,socialSecurityNumber
- Applies
-
Client Response: Filtered, authorized data
- Always use both layers for comprehensive protection
- SensitiveRelationsInterceptor for access control
- PermissionBasedSerializer for data filtering
- Centralize configurations in dedicated files
- Use shared configurations for consistency
- Document permission requirements clearly
- Define specific permissions for different relation types
- Use nested configurations for complex relationships
- Consider
_self
permissions for entity access
-
Mark sensitive fields with appropriate
@Exclude
decorators - Use role-based groups for conditional exclusion
- Document field sensitivity levels
- Test permission scenarios thoroughly
- Verify both authorized and unauthorized access
- Check field filtering for different roles
Problem: Users getting blocked from accessing relations
Solutions:
- Verify user has required permissions
- Check sensitive relations configuration
- Ensure permissions are properly assigned to roles
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
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
- Enable logging in interceptors for troubleshooting
- Test with different user roles to verify filtering
- Use API testing tools to validate responses
- Check permission assignments in database
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
}
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
}
}
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;
}
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
- SensitiveRelationsInterceptor: Minimal overhead, runs before database queries
- PermissionBasedSerializer: Processes after queries, consider caching for large datasets
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;
})
);
}
}
If migrating from older security implementations:
- Replace manual permission checks with interceptor-based approach
-
Update entity decorators to use
@Exclude
instead of manual filtering - Centralize configurations in dedicated files
- Test thoroughly with different user roles and permissions
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
// 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;
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 |
@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
}
@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
}
@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
}
- 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
The Ever-Gauzy security system provides robust, multi-layered protection for sensitive data through:
-
Permission Guards: Method-level access control using
@Permissions
decorators - SensitiveRelationsInterceptor: Relation-specific authorization for entity access
-
Entity Field Protection: Field-level data filtering using
@Exclude
decorators
- 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
- Apply permission guards to controllers using
@UseGuards()
- Configure method permissions with
@Permissions()
decorators - Apply
SensitiveRelationsInterceptor
for relation protection - Configure sensitive relations with
@SensitiveRelations()
- Mark sensitive entity fields with
@Exclude()
decorators - Test with different user permissions and scenarios
- Monitor and audit security events
- 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.
✅ 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
Ever-Gauzy uses a three-layer security approach:
-
Permission Guards (
@Permissions
+ Guards) - Primary access control -
Relation Protection (
@SensitiveRelations
+ Interceptor) - Relation-specific authorization -
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.
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