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:
669
docs/business-processes/SUPPLY_DATA_SECURITY_RULES.md
Normal file
669
docs/business-processes/SUPPLY_DATA_SECURITY_RULES.md
Normal 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)_
|
||||
_Критически важный документ для безопасности коммерческих данных_
|
Reference in New Issue
Block a user