feat(security): Phase 2 - GraphQL Security Integration

 **Основные достижения Phase 2:**
- Создана система автоматической интеграции безопасности через middleware
- Реализованы безопасные версии критических резолверов поставок
- Добавлена фильтрация коммерческих данных по ролям организаций
- Создана подробная документация по интеграции

📁 **Новые файлы:**
- `src/graphql/resolvers/secure-supplies.ts` - безопасные резолверы
- `src/graphql/security/middleware.ts` - автоматическая интеграция
- `src/graphql/resolvers/secure-integration.ts` - демо и интеграция
- `src/graphql/security/INTEGRATION_GUIDE.md` - документация

🔧 **Обновленные файлы:**
- `src/graphql/resolvers/index.ts` - интеграция security middleware
- `src/graphql/security/index.ts` - экспорт middleware функций

🛡️ **Защищенные резолверы:**
- Query: supplyOrders, mySupplyOrders, pendingSuppliesCount
- Mutation: createSupplyOrder, updateSupplyOrderStatus, supplierApproveOrder, supplierRejectOrder, assignLogisticsToSupply

🔒 **Роль-ориентированная фильтрация:**
- SELLER: видит только свои данные с полными ценами
- WHOLESALE: видит заказы со своими товарами без рецептур
- FULFILLMENT: видит назначенные заказы с рецептурами без закупочных цен
- LOGIST: видит только логистическую информацию без коммерческих данных

⚙️ **Feature flags:**
- Автоматическое включение/выключение через ENABLE_SUPPLY_SECURITY
- Градуальный rollout без нарушения работы системы
- Полная обратная совместимость

🎯 **Готово к Phase 3:** Система аудита и мониторинга

🤖 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 18:21:00 +03:00
parent 6ff4ca20db
commit 4529d3c035
6 changed files with 1219 additions and 1 deletions

View File

@ -0,0 +1,306 @@
/**
* Middleware для интеграции системы безопасности в существующие резолверы
*
* Автоматически применяет фильтрацию данных и аудит к резолверам поставок
* без необходимости переписывания всего кода
*/
import { GraphQLError } from 'graphql'
import { OrganizationType } from '@prisma/client'
import { FEATURE_FLAGS } from '../../config/features'
import { SecurityLogger } from '../../lib/security-logger'
import { SupplyDataFilter } from './supply-data-filter'
import { ParticipantIsolation } from './participant-isolation'
import { CommercialDataAudit } from './commercial-data-audit'
import { createSecurityContext } from './index'
import type { SecurityContext, ResourceType, CommercialAccessType } from './types'
/**
* Конфигурация безопасности для резолвера
*/
interface SecurityConfig {
resourceType: ResourceType
auditAction: CommercialAccessType
requiredRole?: OrganizationType[]
enableFiltering: boolean
enableAudit: boolean
enablePartnershipCheck: boolean
}
/**
* Маппинг резолверов на конфигурации безопасности
*/
const RESOLVER_SECURITY_CONFIG: Record<string, SecurityConfig> = {
// Query резолверы
'Query.supplyOrders': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_PRICE',
enableFiltering: true,
enableAudit: true,
enablePartnershipCheck: false,
},
'Query.mySupplyOrders': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_PRICE',
enableFiltering: true,
enableAudit: true,
enablePartnershipCheck: false,
},
'Query.pendingSuppliesCount': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_PRICE',
enableFiltering: false,
enableAudit: true,
enablePartnershipCheck: false,
},
// Mutation резолверы
'Mutation.createSupplyOrder': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_RECIPE',
requiredRole: ['SELLER', 'FULFILLMENT'],
enableFiltering: false,
enableAudit: true,
enablePartnershipCheck: true,
},
'Mutation.updateSupplyOrderStatus': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_PRICE',
enableFiltering: true,
enableAudit: true,
enablePartnershipCheck: false,
},
'Mutation.supplierApproveOrder': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_PRICE',
requiredRole: ['WHOLESALE'],
enableFiltering: true,
enableAudit: true,
enablePartnershipCheck: true,
},
'Mutation.supplierRejectOrder': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_PRICE',
requiredRole: ['WHOLESALE'],
enableFiltering: true,
enableAudit: true,
enablePartnershipCheck: true,
},
'Mutation.assignLogisticsToSupply': {
resourceType: 'SUPPLY_ORDER',
auditAction: 'VIEW_CONTACTS',
requiredRole: ['SELLER', 'FULFILLMENT'],
enableFiltering: true,
enableAudit: true,
enablePartnershipCheck: true,
},
}
/**
* Middleware функция для применения безопасности к резолверу
*/
export function applySecurityMiddleware(
resolverName: string,
originalResolver: Function,
): Function {
const config = RESOLVER_SECURITY_CONFIG[resolverName]
// Если конфигурация не найдена - возвращаем оригинальный резолвер
if (!config) {
return originalResolver
}
return async function securedResolver(parent: unknown, args: unknown, context: unknown, info: unknown) {
// Проверяем включена ли система безопасности
if (!FEATURE_FLAGS.SUPPLY_DATA_SECURITY.enabled) {
return originalResolver(parent, args, context, info)
}
const securityContext = createSecurityContext(context as Record<string, unknown>)
try {
// 1. Проверка аутентификации
if (!securityContext.user.id) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// 2. Проверка роли если требуется
if (config.requiredRole && !config.requiredRole.includes(securityContext.user.organizationType)) {
// Логируем попытку несанкционированного доступа
if (config.enableAudit && FEATURE_FLAGS.SUPPLY_DATA_SECURITY.auditEnabled) {
await CommercialDataAudit.logUnauthorizedAccess((context as any).prisma, {
userId: securityContext.user.id,
organizationType: securityContext.user.organizationType,
resourceType: config.resourceType,
resourceId: 'unknown',
reason: `Insufficient role: ${securityContext.user.organizationType}, required: ${config.requiredRole.join(', ')}`,
ipAddress: securityContext.ipAddress,
userAgent: securityContext.userAgent,
})
}
throw new GraphQLError('Insufficient permissions', {
extensions: { code: 'FORBIDDEN' },
})
}
// 3. Проверка партнерских отношений если требуется
if (config.enablePartnershipCheck && (args as any)?.input?.partnerId) {
try {
await ParticipantIsolation.validatePartnerAccess(
(context as any).prisma,
securityContext.user.organizationId,
(args as any).input.partnerId,
securityContext,
)
} catch (error) {
SecurityLogger.logSecurityError(error as Error, {
operation: 'partnershipCheck',
resolverName,
userId: securityContext.user.id,
organizationType: securityContext.user.organizationType,
})
throw error
}
}
// 4. Логирование доступа
if (config.enableAudit && FEATURE_FLAGS.SUPPLY_DATA_SECURITY.auditEnabled) {
await CommercialDataAudit.logAccess((context as any).prisma, {
userId: securityContext.user.id,
organizationType: securityContext.user.organizationType,
action: config.auditAction,
resourceType: config.resourceType,
metadata: { resolverName, args },
ipAddress: securityContext.ipAddress,
userAgent: securityContext.userAgent,
})
}
// 5. Выполнение оригинального резолвера
let result = await originalResolver(parent, args, context, info)
// 6. Фильтрация результата если включена
if (config.enableFiltering && result && config.resourceType === 'SUPPLY_ORDER') {
result = await filterSupplyOrderResult(result, securityContext)
}
return result
} catch (error) {
SecurityLogger.logSecurityError(error as Error, {
operation: 'securityMiddleware',
resolverName,
resourceType: config.resourceType,
userId: securityContext.user.id,
organizationType: securityContext.user.organizationType,
})
throw error
}
}
}
/**
* Фильтрует результат с заказами поставок
*/
async function filterSupplyOrderResult(
result: unknown,
context: SecurityContext,
): Promise<unknown> {
// Если это массив заказов
if (Array.isArray(result)) {
return Promise.all(
result.map(async (order) => {
if (order && typeof order === 'object' && 'id' in order) {
const filtered = SupplyDataFilter.filterSupplyOrder(order as any, context)
return filtered.data
}
return order
}),
)
}
// Если это одиночный заказ
if (result && typeof result === 'object' && 'id' in result) {
const filtered = SupplyDataFilter.filterSupplyOrder(result as any, context)
return filtered.data
}
// Если это ответ с заказом внутри
if (result && typeof result === 'object' && 'order' in result) {
const resultObj = result as any
if (resultObj.order && typeof resultObj.order === 'object' && 'id' in resultObj.order) {
const filtered = SupplyDataFilter.filterSupplyOrder(resultObj.order, context)
return {
...resultObj,
order: filtered.data,
}
}
}
return result
}
/**
* Автоматически применяет middleware ко всем резолверам из конфигурации
*/
export function wrapResolversWithSecurity(resolvers: Record<string, any>): Record<string, any> {
const wrappedResolvers = { ...resolvers }
// Обрабатываем Query резолверы
if (wrappedResolvers.Query) {
for (const [queryName, resolver] of Object.entries(wrappedResolvers.Query)) {
const resolverName = `Query.${queryName}`
if (RESOLVER_SECURITY_CONFIG[resolverName] && typeof resolver === 'function') {
wrappedResolvers.Query[queryName] = applySecurityMiddleware(resolverName, resolver)
SecurityLogger.logFilteringPerformance({
operation: 'wrapResolver',
duration: 0,
recordsFiltered: 0,
fieldsRemoved: 0,
cacheHit: false,
})
}
}
}
// Обрабатываем Mutation резолверы
if (wrappedResolvers.Mutation) {
for (const [mutationName, resolver] of Object.entries(wrappedResolvers.Mutation)) {
const resolverName = `Mutation.${mutationName}`
if (RESOLVER_SECURITY_CONFIG[resolverName] && typeof resolver === 'function') {
wrappedResolvers.Mutation[mutationName] = applySecurityMiddleware(resolverName, resolver)
}
}
}
return wrappedResolvers
}
/**
* Добавляет новую конфигурацию безопасности для резолвера
*/
export function addSecurityConfig(resolverName: string, config: SecurityConfig): void {
RESOLVER_SECURITY_CONFIG[resolverName] = config
}
/**
* Получает конфигурацию безопасности для резолвера
*/
export function getSecurityConfig(resolverName: string): SecurityConfig | undefined {
return RESOLVER_SECURITY_CONFIG[resolverName]
}
/**
* Выводит список всех защищенных резолверов
*/
export function listSecuredResolvers(): string[] {
return Object.keys(RESOLVER_SECURITY_CONFIG)
}