Files
sfera-new/src/graphql/security/middleware.ts
Veronika Smirnova 4529d3c035 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>
2025-08-22 18:21:00 +03:00

306 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)
}