feat: Phase 1 - Implementation of Data Security Infrastructure

Implemented comprehensive data security infrastructure for SFERA platform:

## Security Classes Created:
- `SupplyDataFilter`: Role-based data filtering for supply orders
- `ParticipantIsolation`: Data isolation between competing organizations
- `RecipeAccessControl`: Protection of production recipes and trade secrets
- `CommercialDataAudit`: Audit logging and suspicious activity detection
- `SecurityLogger`: Centralized security event logging system

## Infrastructure Components:
- Feature flags system for gradual security rollout
- Database migrations for audit logging (AuditLog, SecurityAlert models)
- Secure resolver wrapper for automatic GraphQL security
- TypeScript interfaces and type safety throughout

## Security Features:
- Role-based access control (SELLER, WHOLESALE, FULFILLMENT, LOGIST)
- Commercial data protection between competitors
- Production recipe confidentiality
- Audit trail for all data access
- Real-time security monitoring and alerts
- Rate limiting and suspicious activity detection

## Implementation Notes:
- All console logging replaced with centralized security logger
- Comprehensive TypeScript typing with no explicit 'any' types
- Modular architecture following SFERA coding standards
- Feature flag controlled rollout for safe deployment

This completes Phase 1 of the security implementation plan.
Next phases will integrate these classes into existing GraphQL resolvers.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-22 17:51:02 +03:00
parent e7e4889102
commit 6e3201f491
20 changed files with 5671 additions and 66 deletions

View File

@ -0,0 +1,214 @@
/**
* Обертка для создания безопасных GraphQL резолверов
*
* Обеспечивает автоматическую проверку прав доступа,
* фильтрацию данных и аудит для резолверов
*/
import { OrganizationType, PrismaClient } from '@prisma/client'
import { GraphQLError } from 'graphql'
import { SecurityLogger } from '../../lib/security-logger'
import { CommercialDataAudit } from './commercial-data-audit'
import { ParticipantIsolation } from './participant-isolation'
import { SupplyDataFilter } from './supply-data-filter'
import { SecurityContext, CommercialAccessType, ResourceType } from './types'
/**
* Опции для создания безопасного резолвера
*/
interface SecureResolverOptions {
resourceType: ResourceType
requiredRole?: OrganizationType[]
auditAction: CommercialAccessType
enableFiltering?: boolean
enableAudit?: boolean
}
/**
* Контекст GraphQL с добавленными полями безопасности
*/
interface GraphQLContext {
user?: {
id: string
organizationId: string
organizationType: OrganizationType
}
prisma: unknown // PrismaClient
req?: {
ip?: string
headers?: Record<string, string>
socket?: {
remoteAddress?: string
}
}
}
/**
* Создает безопасную обертку для GraphQL резолвера
*/
export function createSecureResolver<TArgs, TResult>(
resolver: (parent: unknown, args: TArgs, context: GraphQLContext) => Promise<TResult>,
options: SecureResolverOptions,
) {
return async (parent: unknown, args: TArgs, context: GraphQLContext): Promise<TResult> => {
const securityEnabled = process.env.ENABLE_SUPPLY_SECURITY === 'true'
const auditEnabled = process.env.ENABLE_SECURITY_AUDIT === 'true'
// Если система безопасности отключена - выполняем оригинальный резолвер
if (!securityEnabled) {
return resolver(parent, args, context)
}
// Проверка аутентификации
if (!context.user) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const securityContext: SecurityContext = {
user: {
id: context.user.id,
organizationId: context.user.organizationId,
organizationType: context.user.organizationType,
},
ipAddress: context.req?.ip || context.req?.socket?.remoteAddress,
userAgent: context.req?.headers?.['user-agent'],
request: {
headers: context.req?.headers || {},
timestamp: new Date(),
},
}
try {
// Проверка роли если требуется
if (options.requiredRole && !options.requiredRole.includes(context.user.organizationType)) {
// Логируем попытку несанкционированного доступа
if (auditEnabled) {
await CommercialDataAudit.logUnauthorizedAccess(context.prisma, {
userId: context.user.id,
organizationType: context.user.organizationType,
resourceType: options.resourceType,
resourceId: 'unknown',
reason: `Insufficient role: ${context.user.organizationType}, required: ${options.requiredRole.join(', ')}`,
ipAddress: securityContext.ipAddress,
userAgent: securityContext.userAgent,
})
}
throw new GraphQLError('Insufficient permissions', {
extensions: { code: 'FORBIDDEN' },
})
}
// Логирование доступа
if (auditEnabled && options.enableAudit !== false) {
await CommercialDataAudit.logAccess(context.prisma, {
userId: securityContext.user.id,
organizationType: securityContext.user.organizationType,
action: options.auditAction,
resourceType: options.resourceType,
metadata: { args },
ipAddress: securityContext.ipAddress,
userAgent: securityContext.userAgent,
})
}
// Выполнение оригинального резолвера
let result = await resolver(parent, args, context)
// Фильтрация результата если включена
if (options.enableFiltering !== false && result) {
result = await filterResolverResult(result, securityContext, options.resourceType)
}
return result
} catch (error) {
SecurityLogger.logSecurityError(error as Error, {
operation: 'secureResolver',
resourceType: options.resourceType,
userId: context.user.id,
organizationType: context.user.organizationType,
})
throw error
}
}
}
/**
* Фильтрует результат резолвера в зависимости от типа данных
*/
async function filterResolverResult(
result: unknown,
context: SecurityContext,
resourceType: ResourceType,
): Promise<unknown> {
// Если это массив - фильтруем каждый элемент
if (Array.isArray(result)) {
return Promise.all(result.map((item) => filterSingleItem(item, context, resourceType)))
}
// Если это одиночный объект - фильтруем его
return filterSingleItem(result, context, resourceType)
}
/**
* Фильтрует одиночный элемент данных
*/
async function filterSingleItem(item: unknown, context: SecurityContext, resourceType: ResourceType): Promise<unknown> {
switch (resourceType) {
case 'SUPPLY_ORDER':
// Фильтруем данные поставки
if (item && typeof item === 'object' && item.id) {
const filtered = SupplyDataFilter.filterSupplyOrder(item, context)
return filtered.data
}
break
case 'PRODUCT':
case 'SERVICE':
case 'CONSUMABLE':
// Для других типов ресурсов - пока возвращаем как есть
// TODO: добавить специфичную фильтрацию
break
}
return item
}
/**
* Декоратор для автоматического создания безопасного резолвера
*/
export function SecureResolver(options: SecureResolverOptions) {
return function (_target: unknown, _propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value
descriptor.value = createSecureResolver(method, options)
}
}
/**
* Вспомогательные функции для проверки доступа
*/
export const SecurityHelpers = {
/**
* Проверяет доступ к заказу поставки
*/
async checkSupplyOrderAccess(prisma: unknown, orderId: string, context: SecurityContext): Promise<boolean> {
return ParticipantIsolation.validateSupplyOrderAccess(prisma as PrismaClient, orderId, context)
},
/**
* Проверяет партнерские отношения
*/
async checkPartnershipAccess(
prisma: unknown,
organizationId: string,
partnerId: string,
context: SecurityContext,
): Promise<boolean> {
return ParticipantIsolation.validatePartnerAccess(prisma as PrismaClient, organizationId, partnerId, context)
},
}