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

@ -54,20 +54,20 @@ graph TD
**GraphQL мутация подтверждения поставщиком:**
```graphql
# Поставщик может указать детали упаковки при подтверждении
# Поставщик указывает детали упаковки при одобрении (опционально)
mutation SupplierApproveOrderWithPackaging($id: ID!, $packagesCount: Int, $volume: Float) {
supplierApproveOrderWithPackaging(
id: $id
packagesCount: $packagesCount # Количество грузовых мест
volume: $volume # Объём в м³ (влияет на логистические тарифы)
packagesCount: $packagesCount # Опционально: количество грузовых мест
volume: $volume # Опционально: объём в м³ для расчета логистических тарифов
) {
success
message
order {
id
status
packagesCount
volume
packagesCount # null если не указано
volume # null если не указано
}
}
}
@ -247,6 +247,211 @@ createSupplyOrder(input: {
**Обработка входящих заказов:**
```typescript
// Поставщик получает заказы где он является поставщиком
const supplierOrders = await prisma.supplyOrder.findMany({
where: {
partnerId: currentUser.organization.id, // Мы - поставщик
status: 'PENDING', // Ожидает подтверждения
},
})
```
**Действия поставщика:**
```graphql
# Одобрение заказа
mutation SupplierApproveOrder($orderId: ID!) {
supplierApproveOrder(id: $orderId) {
success
order {
id
status
} # PENDING → SUPPLIER_APPROVED
}
}
# Отклонение заказа
mutation SupplierRejectOrder($orderId: ID!, $reason: String) {
supplierRejectOrder(id: $orderId, reason: $reason) {
success
message
}
}
# Отгрузка товара (после подтверждения логистики)
mutation SupplierShipOrder($orderId: ID!) {
supplierShipOrder(id: $orderId) {
success
order {
id
status
} # LOGISTICS_CONFIRMED → SHIPPED
}
}
```
**Компоненты поставщика:**
```typescript
// Техническая реализация кабинета поставщика
src/components/supplier-orders/
├── supplier-orders-dashboard.tsx # Главный dashboard
├── supplier-order-card.tsx # Карточка заказа
├── supplier-orders-tabs.tsx # Табы по статусам
├── supplier-orders-search.tsx # Поиск и фильтры
└── supplier-order-stats.tsx # Статистика заказов
```
**Возможности:**
- ✅ Просматривать входящие заказы (PENDING)
- ✅ Одобрять заказы (PENDING → SUPPLIER_APPROVED)
- ✅ Отклонять заказы (PENDING → CANCELLED)
- ✅ Отгружать товары (LOGISTICS_CONFIRMED → SHIPPED)
- ❌ Изменять детали заказа после создания
- ❌ Видеть заказы других поставщиков
## 🚨 КРИТИЧЕСКИЕ ПРОБЛЕМЫ WORKFLOW
### ВЫЯВЛЕННЫЕ ПРОБЛЕМЫ В ЦЕПОЧКЕ ПОСТАВОК:
#### ❌ **ПРОБЛЕМА 1: Неправильное отображение статусов у поставщика**
```typescript
// ПРОБЛЕМА: Поставщик видит "ожидает подтверждения" вместо только кнопок
// РЕШЕНИЕ: Показывать только кнопки действий, скрывать статусы
// Текущий код (неправильно):
<StatusBadge status={order.status} />
<ActionButtons />
// Правильный код:
{user?.organization?.type === 'WHOLESALE' ? (
<ActionButtons only /> // Только кнопки, без статуса
) : (
<StatusBadge status={order.status} />
)}
```
#### ❌ **ПРОБЛЕМА 2: Отсутствие полей ввода у поставщика**
```typescript
// ПРОБЛЕМА: Поставщик не может указать важные данные при одобрении
interface SupplierPackagingFields {
packagesCount?: number // ОПЦИОНАЛЬНО: Количество грузовых мест
volume?: number // ОПЦИОНАЛЬНО: Объем груза для логистических расчетов
readyDate?: DateTime // ОПЦИОНАЛЬНО: Дата готовности к отгрузке
notes?: string // ОПЦИОНАЛЬНО: Комментарии для логистики
}
// ТРЕБОВАНИЯ:
// ✅ Поля НЕ обязательные - заказ можно одобрить без них
// ✅ Показываются сразу при одобрении для удобства заполнения
// ✅ Используются логистикой для расчета тарифов и планирования
// ✅ Отображаются на 1-м уровне визуализации поставки
// РЕШЕНИЕ: Расширить мутацию supplierApproveOrder
mutation SupplierApproveOrder($input: SupplierApprovalInput!) {
supplierApproveOrder(input: $input) {
success
order {
id, status, packagesCount, volume, readyDate, notes
}
}
}
```
#### ❌ **ПРОБЛЕМА 3: Конфликт статусов в приемке фулфилмента**
```typescript
// КРИТИЧЕСКАЯ ОШИБКА: Резолвер ожидает SHIPPED, но получает SUPPLIER_APPROVED
if (supplyOrder.status !== 'SHIPPED') {
return {
success: false,
message: 'Заказ должен быть в статусе SHIPPED для приемки', // ❌ БЛОКИРУЕТ ПРОЦЕСС
}
}
// РЕШЕНИЕ: Исправить проверку статуса
if (!['SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED'].includes(supplyOrder.status)) {
return {
success: false,
message: 'Заказ должен быть одобрен поставщиком для приемки',
}
}
```
#### ❌ **ПРОБЛЕМА 4: Отсутствие уведомлений поставщика**
```typescript
// ПРОБЛЕМА: Поставщик не знает о новых заказах в реальном времени
// РЕШЕНИЕ: Добавить систему уведомлений
interface SupplierNotifications {
newOrder: 'Новый заказ от {sellerName} на сумму {amount}'
orderCancelled: 'Заказ #{orderNumber} отменен заказчиком'
logistics: 'Логистика подтверждена для заказа #{orderNumber}'
}
```
### ПЛАН ИСПРАВЛЕНИЯ WORKFLOW:
```typescript
interface WorkflowFixes {
// Фаза 1: UI поставщика
supplierInterface: {
hideStatuses: 'Показывать только кнопки действий'
addFields: 'Поля для packagesCount, volume, readyDate'
realtime: 'Уведомления о новых заказах'
}
// Фаза 2: Backend логика
backendLogic: {
expandMutation: 'Расширить supplierApproveOrder с дополнительными полями'
fixStatusCheck: 'Исправить проверку статусов в fulfillmentReceiveOrder'
notifications: 'Система реалтайм уведомлений'
}
// Фаза 3: Интеграция
integration: {
validation: 'Валидация минимальных количеств заказа'
inventory: 'Проверка доступности товаров у поставщика'
logistics: 'Автоматическое назначение логистики'
}
}
```
### ТРЕБОВАНИЯ К РЕАЛИЗАЦИИ:
```typescript
// 1. Исправленная фильтрация заказов для поставщика
const fixedSupplierFilter = `
if (currentUser.organization.type === 'WHOLESALE') {
whereClause = {
partnerId: currentUser.organization.id, // Мы - поставщик
}
} else {
whereClause = {
organizationId: currentUser.organization.id, // Мы - заказчик
}
}
`
// 2. Правильная обработка статусов
const correctStatusHandling = `
// Поставщик видит только кнопки, без статусов
{userRole === 'WHOLESALE' && status === 'PENDING' && (
<ApproveRejectButtons orderId={order.id} />
)}
// Остальные видят статусы
{userRole !== 'WHOLESALE' && (
<StatusBadge status={status} />
)}
`
```
```typescript
// Из кода resolvers.ts:
const incomingSupplierOrders = await prisma.supplyOrder.count({

View File

@ -0,0 +1,669 @@
# ПРАВИЛА БЕЗОПАСНОСТИ ДАННЫХ В ПОСТАВКАХ SFERA
## 🎯 ОБЗОР
Система безопасности данных в поставках обеспечивает **коммерческую конфиденциальность** и **изоляцию данных** между участниками цепочки поставок: SELLER, WHOLESALE, FULFILLMENT, LOGIST.
### КЛЮЧЕВЫЕ ПРИНЦИПЫ:
1. **Принцип минимальных привилегий** - каждый участник видит только необходимые данные
2. **Коммерческая тайна** - защита закупочных цен и производственных секретов
3. **Изоляция данных** - участники не видят данные друг друга
4. **Аудит доступа** - логирование всех обращений к чувствительным данным
## 🔐 МАТРИЦА ДОСТУПА К ДАННЫМ
### СТРУКТУРА ДАННЫХ ПОСТАВКИ:
```typescript
interface SupplyOrder {
// Базовая информация (видна всем участникам)
id: string
status: SupplyOrderStatus
deliveryDate: Date
totalItems: number
// Коммерческая информация (ограниченный доступ)
productPrice: Decimal // Закупочная цена у поставщика
fulfillmentServicePrice: Decimal // Стоимость услуг ФФ
logisticsPrice: Decimal // Стоимость доставки
totalAmount: Decimal // Общая сумма
// Производственная информация (ограниченный доступ)
recipe: {
services: Service[] // Услуги ФФ
fulfillmentConsumables: Supply[] // Расходники ФФ
sellerConsumables: Supply[] // Расходники селлера
}
// Упаковочная информация (опциональная)
packagesCount?: number // Количество грузовых мест
volume?: number // Объем груза в м³
readyDate?: Date // Дата готовности к отгрузке
notes?: string // Комментарии
}
```
### ТАБЛИЦА ДОСТУПА:
| Данные | SELLER | WHOLESALE | FULFILLMENT | LOGIST |
| ---------------------------------- | ------ | --------- | ----------- | ------ |
| **Базовая информация** | ✅ | ✅ | ✅ | ✅ |
| **productPrice** (закупочная цена) | ✅ | ✅ | ❌ | ❌ |
| **fulfillmentServicePrice** | ✅ | ❌ | ✅ | ❌ |
| **logisticsPrice** | ✅ | ❌ | ✅ | ✅ |
| **totalAmount для SELLER** | ✅ | ❌ | ❌ | ❌ |
| **totalAmount для FULFILLMENT** | ❌ | ❌ | ✅ | ❌ |
| **recipe (рецептура)** | ✅ | ❌ | ✅ | ❌ |
| **packagesCount, volume** | ✅ | ✅ | ✅ | ✅ |
| **Контакты других участников** | ❌ | ❌ | ❌ | ❌ |
## 📊 РАСЧЕТ СТОИМОСТЕЙ ПО РОЛЯМ
### ДЛЯ SELLER (полная стоимость):
```typescript
totalAmountForSeller =
productPrice + // Закупка у поставщика
fulfillmentServicePrice + // Услуги ФФ
logisticsPrice + // Доставка
fulfillmentConsumablesPrice + // Расходники ФФ
sellerConsumablesPrice // Свои расходники (price × quantity)
```
### ДЛЯ FULFILLMENT (без закупочных цен):
```typescript
totalAmountForFulfillment =
fulfillmentServicePrice + // Свои услуги
logisticsPrice + // Доставка (для планирования)
fulfillmentConsumablesPrice // Свои расходники
// НЕ ВИДИТ: productPrice, sellerConsumablesPrice
```
### ДЛЯ WHOLESALE (только свои товары):
```typescript
totalAmountForWholesale =
productPrice × quantity // Только стоимость своих товаров
// НЕ ВИДИТ: услуги ФФ, логистику, рецептуру
```
### ДЛЯ LOGIST (только доставка):
```typescript
totalAmountForLogist = logisticsPrice // Только стоимость доставки
// НЕ ВИДИТ: цены товаров, услуги, рецептуру
```
## 🛡️ РЕАЛИЗАЦИЯ БЕЗОПАСНОСТИ
### 1. ФИЛЬТРАЦИЯ НА УРОВНЕ RESOLVER
```typescript
// src/graphql/security/supply-data-filter.ts
export class SupplyDataFilter {
/**
* Фильтрует данные поставки в зависимости от роли пользователя
*/
static filterSupplyOrderByRole(order: SupplyOrder, userRole: OrganizationType, userId: string): FilteredSupplyOrder {
switch (userRole) {
case 'SELLER':
return this.filterForSeller(order, userId)
case 'WHOLESALE':
return this.filterForWholesale(order, userId)
case 'FULFILLMENT':
return this.filterForFulfillment(order, userId)
case 'LOGIST':
return this.filterForLogist(order, userId)
default:
throw new GraphQLError('Unauthorized organization type')
}
}
/**
* SELLER видит всю информацию по своим поставкам
*/
private static filterForSeller(order: SupplyOrder, userId: string): FilteredSupplyOrder {
// Проверка, что это поставка данного селлера
if (order.organizationId !== userId) {
throw new GraphQLError('Access denied to this supply order')
}
return {
...order,
// Селлер видит все данные своей поставки
}
}
/**
* WHOLESALE видит только свои товары без рецептуры
*/
private static filterForWholesale(order: SupplyOrder, userId: string): FilteredSupplyOrder {
// Фильтруем только позиции данного поставщика
const myItems = order.items.filter((item) => item.product.organizationId === userId)
if (myItems.length === 0) {
throw new GraphQLError('No items from your organization in this order')
}
return {
...order,
items: myItems.map((item) => ({
...item,
// Убираем рецептуру
recipe: null,
services: [],
fulfillmentConsumables: [],
sellerConsumables: [],
})),
// Скрываем общие суммы и услуги
totalAmount: null,
fulfillmentServicePrice: null,
logisticsPrice: null,
// Оставляем информацию об упаковке
packagesCount: order.packagesCount,
volume: order.volume,
}
}
/**
* FULFILLMENT видит рецептуру, но не видит закупочные цены
*/
private static filterForFulfillment(order: SupplyOrder, userId: string): FilteredSupplyOrder {
// Проверка, что поставка для данного ФФ
if (order.fulfillmentCenterId !== userId) {
throw new GraphQLError('Access denied to this supply order')
}
return {
...order,
items: order.items.map((item) => ({
...item,
// Скрываем закупочные цены
price: null,
productPrice: null,
// Оставляем рецептуру
recipe: item.recipe,
// Для расходников селлера показываем только ID и количество
sellerConsumables: item.sellerConsumables?.map((c) => ({
id: c.id,
name: c.name,
quantity: c.quantity,
// НЕ показываем цену
})),
})),
// Показываем только свою часть общей суммы
totalAmount: this.calculateFulfillmentTotal(order),
productPrice: null, // Скрыто
}
}
/**
* LOGIST видит только информацию о доставке
*/
private static filterForLogist(order: SupplyOrder, userId: string): FilteredSupplyOrder {
// Проверка, что логистика назначена на этот заказ
if (order.logisticsPartnerId !== userId) {
throw new GraphQLError('Access denied to this supply order')
}
return {
// Базовая информация
id: order.id,
status: order.status,
deliveryDate: order.deliveryDate,
// Информация о маршруте
routes: order.routes.map((route) => ({
from: route.from,
fromAddress: route.fromAddress,
to: route.to,
toAddress: route.toAddress,
// Только количество мест и объем
packagesCount: route.packagesCount,
volume: route.volume,
})),
// Только логистическая информация
logisticsPrice: order.logisticsPrice,
totalAmount: order.logisticsPrice, // Только своя сумма
// Скрываем все остальное
items: [],
recipe: null,
productPrice: null,
fulfillmentServicePrice: null,
}
}
/**
* Расчет суммы для фулфилмента
*/
private static calculateFulfillmentTotal(order: SupplyOrder): number {
return (
Number(order.fulfillmentServicePrice || 0) +
Number(order.logisticsPrice || 0) +
order.items.reduce((sum, item) => {
const consumablesPrice =
item.fulfillmentConsumables?.reduce((cSum, c) => cSum + c.pricePerUnit * c.quantity, 0) || 0
return sum + consumablesPrice
}, 0)
)
}
}
```
### 2. ИЗОЛЯЦИЯ ДАННЫХ МЕЖДУ УЧАСТНИКАМИ
```typescript
// src/graphql/security/participant-isolation.ts
export class ParticipantIsolation {
/**
* Проверяет, что селлеры не видят данные друг друга
*/
static async validateSellerIsolation(
prisma: PrismaClient,
currentUserId: string,
targetSellerId: string,
): Promise<boolean> {
// Селлер может видеть только свои данные
if (currentUserId !== targetSellerId) {
throw new GraphQLError('Access denied to other seller data')
}
return true
}
/**
* Проверяет доступ к данным через партнерство
*/
static async validatePartnerAccess(
prisma: PrismaClient,
organizationId: string,
partnerId: string,
): Promise<boolean> {
const partnership = await prisma.counterparty.findFirst({
where: {
OR: [
{
organizationId: organizationId,
counterpartyId: partnerId,
status: 'ACCEPTED',
},
{
organizationId: partnerId,
counterpartyId: organizationId,
status: 'ACCEPTED',
},
],
},
})
if (!partnership) {
throw new GraphQLError('No active partnership found')
}
return true
}
/**
* Группировка заказов для логистики с изоляцией селлеров
*/
static groupOrdersForLogistics(orders: SupplyOrder[]): GroupedLogisticsOrder[] {
// Группируем по маршрутам, скрывая информацию о селлерах
const grouped = orders.reduce(
(acc, order) => {
const routeKey = `${order.route.from}-${order.route.to}`
if (!acc[routeKey]) {
acc[routeKey] = {
route: {
from: order.route.from,
to: order.route.to,
},
orders: [],
totalPackages: 0,
totalVolume: 0,
}
}
// Добавляем заказ БЕЗ информации о селлере
acc[routeKey].orders.push({
id: order.id,
packagesCount: order.packagesCount || 0,
volume: order.volume || 0,
// НЕ добавляем: organizationId, sellerName и т.д.
})
acc[routeKey].totalPackages += order.packagesCount || 0
acc[routeKey].totalVolume += order.volume || 0
return acc
},
{} as Record<string, GroupedLogisticsOrder>,
)
return Object.values(grouped)
}
}
```
### 3. КОНТРОЛЬ ДОСТУПА К РЕЦЕПТУРЕ
```typescript
// src/graphql/security/recipe-access-control.ts
export class RecipeAccessControl {
/**
* Фильтрует рецептуру в зависимости от роли
*/
static filterRecipeByRole(
recipe: ProductRecipe,
userRole: OrganizationType,
userOrgId: string,
fulfillmentId?: string,
): FilteredRecipe | null {
switch (userRole) {
case 'SELLER':
// Селлер видит полную рецептуру
return recipe
case 'FULFILLMENT':
// ФФ видит рецептуру только если это его заказ
if (fulfillmentId === userOrgId) {
return {
services: recipe.services,
fulfillmentConsumables: recipe.fulfillmentConsumables.map((c) => ({
...c,
// Показываем pricePerUnit для расчета, НЕ закупочную цену
price: undefined,
pricePerUnit: c.pricePerUnit,
})),
sellerConsumables: recipe.sellerConsumables.map((c) => ({
id: c.id,
name: c.name,
quantity: c.quantity,
// НЕ показываем цены расходников селлера
})),
}
}
return null
case 'WHOLESALE':
case 'LOGIST':
// Поставщик и логистика НЕ видят рецептуру
return null
default:
return null
}
}
/**
* Проверяет доступ к услугам фулфилмента
*/
static async validateServiceAccess(
prisma: PrismaClient,
serviceIds: string[],
fulfillmentId: string,
): Promise<boolean> {
const services = await prisma.service.findMany({
where: {
id: { in: serviceIds },
organizationId: fulfillmentId,
},
})
if (services.length !== serviceIds.length) {
throw new GraphQLError('Some services do not belong to this fulfillment center')
}
return true
}
}
```
### 4. АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
```typescript
// src/graphql/security/commercial-data-audit.ts
export class CommercialDataAudit {
/**
* Логирует доступ к коммерческим данным
*/
static async logAccess(params: {
userId: string
organizationType: OrganizationType
accessType: 'VIEW_PRICE' | 'VIEW_RECIPE' | 'VIEW_CONTACTS'
resourceType: 'SUPPLY_ORDER' | 'PRODUCT' | 'SERVICE'
resourceId: string
metadata?: Record<string, any>
}): Promise<void> {
const { userId, organizationType, accessType, resourceType, resourceId, metadata } = params
// Критические типы доступа требующие особого внимания
const criticalAccess = [
'VIEW_PRICE', // Просмотр коммерческих цен
'VIEW_RECIPE', // Просмотр производственных секретов
]
if (criticalAccess.includes(accessType)) {
console.warn(
`🔐 CRITICAL DATA ACCESS: User ${userId} (${organizationType}) accessed ${accessType} for ${resourceType} ${resourceId}`,
)
}
// Сохраняем в базу данных
await prisma.auditLog.create({
data: {
userId,
organizationType,
action: `DATA_ACCESS:${accessType}`,
resourceType,
resourceId,
metadata: metadata || {},
ipAddress: metadata?.ipAddress,
userAgent: metadata?.userAgent,
timestamp: new Date(),
},
})
// Проверка на подозрительную активность
await this.checkSuspiciousActivity(userId, accessType)
}
/**
* Проверка подозрительной активности
*/
private static async checkSuspiciousActivity(userId: string, accessType: string): Promise<void> {
// Считаем количество обращений за последний час
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000)
const accessCount = await prisma.auditLog.count({
where: {
userId,
action: { contains: accessType },
timestamp: { gte: oneHourAgo },
},
})
// Пороги для разных типов доступа
const thresholds = {
VIEW_PRICE: 100, // Максимум 100 просмотров цен в час
VIEW_RECIPE: 50, // Максимум 50 просмотров рецептур в час
VIEW_CONTACTS: 200, // Максимум 200 просмотров контактов в час
}
if (accessCount > thresholds[accessType]) {
// Отправляем алерт администраторам
await this.sendSecurityAlert({
userId,
type: 'EXCESSIVE_DATA_ACCESS',
message: `User ${userId} exceeded ${accessType} threshold: ${accessCount} accesses in 1 hour`,
severity: 'HIGH',
})
}
}
/**
* Отправка алертов безопасности
*/
private static async sendSecurityAlert(alert: {
userId: string
type: string
message: string
severity: 'LOW' | 'MEDIUM' | 'HIGH'
}): Promise<void> {
console.error(`🚨 SECURITY ALERT [${alert.severity}]: ${alert.message}`)
// TODO: Интеграция с системой алертов (email, SMS, Slack)
// await notificationService.sendAlert(alert)
}
}
```
## 🔒 ПРАКТИЧЕСКИЕ ПРИМЕРЫ
### ПРИМЕР 1: Селлер создает поставку товаров
```typescript
// Селлер видит полную информацию
{
"id": "supply-001",
"status": "PENDING",
"items": [{
"product": { "name": "Товар A", "price": 1000 }, // ✅ Видит закупочную цену
"quantity": 10,
"recipe": { // ✅ Видит рецептуру
"services": ["Упаковка", "Маркировка"],
"fulfillmentConsumables": ["Пленка", "Скотч"],
"sellerConsumables": ["Этикетка бренда"]
}
}],
"totalAmount": 15000, // ✅ Видит полную сумму
"productPrice": 10000,
"fulfillmentServicePrice": 3000,
"logisticsPrice": 2000
}
```
### ПРИМЕР 2: Поставщик видит тот же заказ
```typescript
// Поставщик видит только свою часть
{
"id": "supply-001",
"status": "PENDING",
"deliveryDate": "2024-01-15",
"items": [{
"product": { "name": "Товар A", "price": 1000 }, // ✅ Видит свою цену
"quantity": 10
// ❌ НЕ видит recipe
}],
"packagesCount": 2, // ✅ Видит упаковочную информацию
"volume": 0.5,
// ❌ НЕ видит totalAmount, услуги ФФ, логистику
}
```
### ПРИМЕР 3: Фулфилмент видит тот же заказ
```typescript
// Фулфилмент видит рецептуру без закупочных цен
{
"id": "supply-001",
"status": "PENDING",
"items": [{
"product": { "name": "Товар A" }, // ❌ НЕ видит закупочную цену
"quantity": 10,
"recipe": { // ✅ Видит рецептуру
"services": ["Упаковка", "Маркировка"],
"fulfillmentConsumables": [{
"name": "Пленка",
"pricePerUnit": 50 // ✅ Видит свою цену расходника
}],
"sellerConsumables": [{
"name": "Этикетка бренда",
"quantity": 10
// ❌ НЕ видит цену расходников селлера
}]
}
}],
"totalAmount": 5000, // ✅ Только сумма услуг ФФ + логистика + расходники ФФ
"fulfillmentServicePrice": 3000,
"logisticsPrice": 2000
}
```
### ПРИМЕР 4: Логистика видит только доставку
```typescript
// Логистика видит минимум информации
{
"id": "supply-001",
"status": "LOGISTICS_CONFIRMED",
"routes": [{
"from": "Склад поставщика",
"fromAddress": "ул. Садовая, 1",
"to": "Фулфилмент центр",
"toAddress": "ул. Складская, 10",
"packagesCount": 2, // ✅ Видит количество мест
"volume": 0.5 // ✅ Видит объем
}],
"logisticsPrice": 2000, // ✅ Видит только свою стоимость
// ❌ НЕ видит товары, цены, рецептуру, участников
}
```
## ⚠️ КРИТИЧЕСКИЕ ПРАВИЛА БЕЗОПАСНОСТИ
### 1. НИКОГДА НЕ ПОКАЗЫВАТЬ:
- **Фулфилменту** - закупочные цены поставщика (`productPrice`)
- **Поставщику** - рецептуру и услуги фулфилмента
- **Логистике** - коммерческую информацию и рецептуру
- **Селлерам** - данные других селлеров
### 2. ВСЕГДА ПРОВЕРЯТЬ:
- Партнерские отношения перед доступом к данным
- Принадлежность заказа текущей организации
- Роль пользователя перед фильтрацией данных
- Подозрительную активность в логах
### 3. ОБЯЗАТЕЛЬНО ЛОГИРОВАТЬ:
- Все обращения к коммерческим данным
- Попытки несанкционированного доступа
- Массовые запросы данных
- Изменения критических полей
## 🛠️ IMPLEMENTATION CHECKLIST
- [ ] Реализовать `SupplyDataFilter` класс для фильтрации по ролям
- [ ] Добавить `ParticipantIsolation` для изоляции участников
- [ ] Внедрить `RecipeAccessControl` для контроля рецептур
- [ ] Настроить `CommercialDataAudit` для аудита
- [ ] Обновить GraphQL резолверы с новыми фильтрами
- [ ] Добавить тесты безопасности для каждой роли
- [ ] Настроить мониторинг и алерты
- [ ] Провести security review кода
## 📚 СВЯЗАННЫЕ ДОКУМЕНТЫ
- [SECURITY_PRACTICES.md](../infrastructure/SECURITY_PRACTICES.md) - Общие практики безопасности
- [SUPPLY_CHAIN_WORKFLOW.md](./SUPPLY_CHAIN_WORKFLOW.md) - Workflow поставок
- [GRAPHQL_SCHEMA_RULES.md](../api-layer/GRAPHQL_SCHEMA_RULES.md) - Правила GraphQL API
---
ата создания: 2025-08-22_
_Автор: Claude (Anthropic)_
_Критически важный документ для безопасности коммерческих данных_