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:
214
src/graphql/security/secure-resolver.ts
Normal file
214
src/graphql/security/secure-resolver.ts
Normal 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)
|
||||
},
|
||||
}
|
Reference in New Issue
Block a user