
✅ **Основные достижения 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>
306 lines
10 KiB
TypeScript
306 lines
10 KiB
TypeScript
/**
|
||
* 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)
|
||
} |