
- Добавлены колонки Объём и Грузовые места между Цена товаров и Статус - Реализованы инпуты для ввода volume и packagesCount в статусе PENDING для роли WHOLESALE - Добавлена мутация UPDATE_SUPPLY_PARAMETERS с проверками безопасности - Скрыта строка Поставщик для роли WHOLESALE (поставщик знает свои данные) - Исправлено выравнивание таблицы при скрытии уровня поставщика - Реорганизованы документы: legacy-rules/, docs/, docs-and-reports/ ВНИМАНИЕ: Компонент multilevel-supplies-table.tsx (1697 строк) нарушает правило модульной архитектуры (>800 строк требует рефакторинга) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
670 lines
23 KiB
Markdown
670 lines
23 KiB
Markdown
# ПРАВИЛА БЕЗОПАСНОСТИ ДАННЫХ В ПОСТАВКАХ 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)_
|
||
_Критически важный документ для безопасности коммерческих данных_
|