bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
567 lines
19 KiB
Markdown
567 lines
19 KiB
Markdown
# ARC-003 — Tenant Isolation Standard
|
|
|
|
**Status:** Accepted
|
|
**Datum:** 2026-06-01
|
|
**Beslutsfattare:** AAMOS Architecture Team
|
|
|
|
---
|
|
|
|
## Kontext
|
|
|
|
AAMOS driftar ekonomidata för flera separata juridiska och organisatoriska entiteter. Läckage av data mellan tenants är ett **kritiskt fel** — dels juridiskt (bokföringssekretess, GDPR), dels affärsmässigt (förtroende). Ett enstaka cross-tenant-dataläckage kan vara fatal för produkten.
|
|
|
|
Idag implementeras isolering uteslutande på applikationsnivå. Detta är nödvändigt men inte tillräckligt som enda försvarslinje. Standardet definierar **defense in depth** för alla lager.
|
|
|
|
Ledger Engine kör idag på `:3250` med `tenant_id` på varje rad — korrekt ansats, men utan formaliserat kontrakt. Hermes Event Fabric har `tenant_id` i envelope — korrekt. Detta dokument formaliserar dessa val och utökar dem till hela stacken.
|
|
|
|
---
|
|
|
|
## Beslut
|
|
|
|
### 1. Tenant-definition och hierarki
|
|
|
|
#### Vad är en tenant?
|
|
|
|
En **tenant** är den primära isoleringsdomänen i AAMOS. Den representerar en oberoende ekonomisk och juridisk enhet vars data **aldrig** ska vara tillgänglig för en annan tenant, oavsett konfiguration.
|
|
|
|
```
|
|
tenant_id (UUID v7)
|
|
├── Primär nyckel i alla tabeller
|
|
├── Korresponderar mot en juridisk/operativ entitet
|
|
├── Är oföränderlig efter skapande
|
|
└── Kan INTE delegeras nedåt i hierarkin
|
|
```
|
|
|
|
#### Hierarki
|
|
|
|
```
|
|
Tenant (isoleringsdomän — hårdgräns)
|
|
└── Organization (organisatorisk enhet inom tenant — ARC-001)
|
|
├── org_type = "company" (juridisk person)
|
|
├── org_type = "department" (avdelning)
|
|
├── org_type = "team" (team)
|
|
└── org_type = "subsidiary" (dotterbolag inom samma tenant)
|
|
```
|
|
|
|
**Viktigt:**
|
|
- Dotterbolag inom samma juridiska koncern → **samma tenant**, separerade via `Organization.parent_id`
|
|
- Separata juridiska entiteter med oberoende bokföring → **separata tenants**
|
|
- En person (Person.id) kan existera i **multipla tenants** — men med separata Person-rader per tenant
|
|
|
|
#### Tenant-livscykel
|
|
|
|
```
|
|
provisioning → active → suspended → terminated
|
|
```
|
|
|
|
- `terminated`-tenants: data locked, 90 dagars cooling period, sedan arkivering eller borttagning per avtal
|
|
- Suspended-tenants: läsåtkomst tillåten för ägare, inga skrivoperationer
|
|
|
|
---
|
|
|
|
### 2. Dataklassificering — 4 nivåer
|
|
|
|
| Nivå | Etikett | Definition | Kryptering i vila | Kryptering i transit |
|
|
|------|---------|------------|-------------------|----------------------|
|
|
| 0 | `public` | Kan delas fritt utan risk | Ej krav | TLS |
|
|
| 1 | `internal` | Intern information, ej känslig | Rekommenderat | TLS |
|
|
| 2 | `confidential` | Affärskritisk, skyddad | **Obligatoriskt** | TLS + mTLS rekommenderat |
|
|
| 3 | `restricted` | Personuppgifter, juridisk risk | **Obligatoriskt + field-level** | TLS + mTLS |
|
|
|
|
#### Ekonomimodulens dataklassificering
|
|
|
|
| Dataobjekt | Nivå | Motivering |
|
|
|------------|------|------------|
|
|
| Kontoplan (struktur) | `internal` | Strukturinformation, ej känslig |
|
|
| Transaktioner (belopp, konto) | `confidential` | Affärskritisk bokföring |
|
|
| Leverantörsfakturor | `confidential` | Affärskritisk |
|
|
| Bankkopplingar, bankuppgifter | `restricted` | Finansiell känslig info |
|
|
| Personuppgifter (Person.national_id) | `restricted` | GDPR Art. 9 |
|
|
| Lönedata | `restricted` | GDPR + arbetsrättslig känslighet |
|
|
| Kundsaldo, kreditgräns | `confidential` | Affärskritisk |
|
|
| Revisionslogg (audit trail) | `confidential` | Skyddad men inte personuppgift |
|
|
| Inloggningsuppgifter | `restricted` | Autentiseringsdata |
|
|
|
|
---
|
|
|
|
### 3. Isolering per lager
|
|
|
|
#### 3.1 PostgreSQL — Row-Level Security (RLS)
|
|
|
|
**Princip:** Databas ska vara sista försvarslinjen. Även om applikationen har en bugg ska databasen neka cross-tenant-åtkomst.
|
|
|
|
**Implementation:**
|
|
|
|
```sql
|
|
-- Aktivera RLS på alla tabeller med tenant_id
|
|
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE transactions FORCE ROW LEVEL SECURITY; -- Gäller även superuser
|
|
|
|
-- Policy: läsning
|
|
CREATE POLICY tenant_isolation_select ON transactions
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
|
|
-- Policy: insert
|
|
CREATE POLICY tenant_isolation_insert ON transactions
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
|
|
-- Policy: update
|
|
CREATE POLICY tenant_isolation_update ON transactions
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id')::uuid)
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
|
|
-- Policy: delete (inkl. soft-delete)
|
|
CREATE POLICY tenant_isolation_delete ON transactions
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
```
|
|
|
|
**Sätta tenant-kontext i applikationen (connection-level):**
|
|
|
|
```javascript
|
|
// I middleware, FÖRE varje databasanrop
|
|
async function setTenantContext(client, tenantId) {
|
|
// Validera UUID-format
|
|
if (!isValidUUID(tenantId)) {
|
|
throw new SecurityError('INVALID_TENANT_ID', tenantId);
|
|
}
|
|
await client.query(
|
|
`SELECT set_config('app.current_tenant_id', $1, true)`,
|
|
[tenantId]
|
|
);
|
|
}
|
|
|
|
// Express middleware
|
|
app.use(async (req, res, next) => {
|
|
const tenantId = req.headers['x-tenant-id'] || req.user?.tenant_id;
|
|
if (!tenantId) return res.status(401).json({ error: 'TENANT_ID_REQUIRED' });
|
|
|
|
req.db = await pool.connect();
|
|
await setTenantContext(req.db, tenantId);
|
|
|
|
res.on('finish', () => req.db.release());
|
|
next();
|
|
});
|
|
```
|
|
|
|
**RLS-aktivering för befintliga tabeller (Ekonomimodulen):**
|
|
|
|
```sql
|
|
-- Skapa en funktion för att enablea RLS på alla tabeller i ett schema
|
|
CREATE OR REPLACE FUNCTION enable_rls_for_schema(schema_name text)
|
|
RETURNS void AS $$
|
|
DECLARE
|
|
tbl text;
|
|
BEGIN
|
|
FOR tbl IN
|
|
SELECT tablename FROM pg_tables
|
|
WHERE schemaname = schema_name
|
|
AND tablename NOT IN ('schema_migrations', 'capability_registry')
|
|
LOOP
|
|
EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', schema_name, tbl);
|
|
EXECUTE format('ALTER TABLE %I.%I FORCE ROW LEVEL SECURITY', schema_name, tbl);
|
|
EXECUTE format(
|
|
'CREATE POLICY IF NOT EXISTS tenant_isolation ON %I.%I
|
|
USING (tenant_id = current_setting(''app.current_tenant_id'')::uuid)',
|
|
schema_name, tbl
|
|
);
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Kör för ekonomi-schema:
|
|
SELECT enable_rls_for_schema('ekonomi');
|
|
```
|
|
|
|
**Undantag från RLS:**
|
|
Tabeller utan `tenant_id` (globala konfigurationstabeller) skyddas via separat `is_global = true`-policy och `pg_roles`-baserad åtkomststyrning.
|
|
|
|
---
|
|
|
|
#### 3.2 Redis — Key-prefix schema
|
|
|
|
**Obligatoriskt nyckelformat:**
|
|
|
|
```
|
|
{tenant_id}:{service}:{data_type}:{identifier}
|
|
```
|
|
|
|
**Exempel:**
|
|
|
|
```
|
|
# Korrekt
|
|
f47ac10b-58cc-4372-a567-0e02b2c3d479:ledger:session:user_123
|
|
f47ac10b-58cc-4372-a567-0e02b2c3d479:ledger:cache:accounts
|
|
f47ac10b-58cc-4372-a567-0e02b2c3d479:hermes:queue:pending
|
|
|
|
# FEL — saknar tenant-prefix
|
|
ledger:cache:accounts
|
|
session:user_123
|
|
```
|
|
|
|
**Implementation i Node.js:**
|
|
|
|
```javascript
|
|
class TenantRedisClient {
|
|
constructor(redisClient, tenantId) {
|
|
this.client = redisClient;
|
|
this.tenantId = tenantId;
|
|
this.validateTenantId(tenantId);
|
|
}
|
|
|
|
key(service, dataType, identifier) {
|
|
return `${this.tenantId}:${service}:${dataType}:${identifier}`;
|
|
}
|
|
|
|
async get(service, dataType, identifier) {
|
|
return this.client.get(this.key(service, dataType, identifier));
|
|
}
|
|
|
|
async set(service, dataType, identifier, value, ttl) {
|
|
const k = this.key(service, dataType, identifier);
|
|
if (ttl) return this.client.setex(k, ttl, value);
|
|
return this.client.set(k, value);
|
|
}
|
|
|
|
// Scan aldrig utan tenant-prefix — blockerat
|
|
async scan(pattern) {
|
|
const safePattern = `${this.tenantId}:${pattern}`;
|
|
return this.client.scan(0, 'MATCH', safePattern, 'COUNT', 100);
|
|
}
|
|
|
|
validateTenantId(id) {
|
|
if (!/^[0-9a-f-]{36}$/i.test(id)) {
|
|
throw new SecurityError('INVALID_TENANT_ID_FORMAT');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Redis-isolering vid multi-tenant-miljö (production):**
|
|
Överväg separata Redis-databaser (`SELECT 0..15`) eller separata Redis-instanser per tenant-grupp för stark isolering om tenant-volym tillåter det.
|
|
|
|
---
|
|
|
|
#### 3.3 Hermes Event Fabric — Tenant-envelope
|
|
|
|
**Event-envelope (redan implementerat, formaliseras här):**
|
|
|
|
```json
|
|
{
|
|
"envelope": {
|
|
"event_id": "uuid-v7",
|
|
"tenant_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
"event_type": "aamos.ekonomi.transaction.posted",
|
|
"event_version": "1.0.0",
|
|
"source_service": "ledger-engine",
|
|
"correlation_id": "uuid",
|
|
"causation_id": "uuid|null",
|
|
"published_at": "2026-06-01T19:00:00Z"
|
|
},
|
|
"payload": { ... }
|
|
}
|
|
```
|
|
|
|
**Enforcement-regler:**
|
|
|
|
1. **Subscription-filtrering:** Varje konsument prenumererar på `{tenant_id}:*` — aldrig på `*` utan prefix
|
|
2. **Consumer-validering:** Konsumenten MÅSTE validera `envelope.tenant_id` mot sin egen kontext vid mottagande
|
|
3. **Ingen cross-tenant-routing:** Event-router får aldrig leverera ett event till en konsument med annan `tenant_id`
|
|
4. **JSONL-fallback:** Filer namnges `{tenant_id}/{date}/{service}.jsonl` — aldrig mixade tenant-filer
|
|
|
|
```javascript
|
|
// Hermes consumer-mönster
|
|
hermes.subscribe(`${tenantId}:aamos.ekonomi.#`, async (message) => {
|
|
// Alltid validera — defense in depth
|
|
if (message.envelope.tenant_id !== tenantId) {
|
|
await auditLog.critical('CROSS_TENANT_EVENT_RECEIVED', {
|
|
expected: tenantId,
|
|
received: message.envelope.tenant_id,
|
|
event_id: message.envelope.event_id
|
|
});
|
|
throw new SecurityError('CROSS_TENANT_EVENT');
|
|
}
|
|
// Process event...
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
#### 3.4 Search / Vector — Namespacing för framtida semantic search
|
|
|
|
**Princip för framtida implementation:**
|
|
|
|
```
|
|
Namespace-format: {tenant_id}_{collection}
|
|
Exempel: f47ac10b_transactions, f47ac10b_documents
|
|
|
|
Sökning MÅSTE alltid inkludera tenant_id-filter:
|
|
{
|
|
"filter": { "must": [{ "key": "tenant_id", "match": { "value": "{tenant_id}" } }] },
|
|
"query_vector": [...]
|
|
}
|
|
```
|
|
|
|
**Krav vid val av vector store:**
|
|
- Stöd för metadata-filtrering FÖRE nearest-neighbor-sökning (pre-filter, ej post-filter)
|
|
- Namespacing på collection-nivå som alternativ
|
|
- Audit-loggning av alla vektorsökningar med tenant-kontext
|
|
|
|
---
|
|
|
|
#### 3.5 AI-kontext och GDPR — Regler för prompt-innehåll
|
|
|
|
**Konkreta regler för vad som får finnas i en LLM-prompt:**
|
|
|
|
| Datatyp | Tillåtet i prompt? | Krav om tillåtet |
|
|
|---------|-------------------|------------------|
|
|
| Transaktionsbelopp (aggregerat) | ✅ Ja | Utan individuell koppling |
|
|
| Kontonummer (ej bank) | ✅ Ja | Internt kontonummer |
|
|
| Leverantörsnamn | ✅ Ja | |
|
|
| Person.name | ⚠️ Begränsat | Bara om nödvändigt + pseudonymiserat |
|
|
| Person.national_id | ❌ Nej | Aldrig |
|
|
| Person.email, phone | ❌ Nej | Aldrig i raw-form |
|
|
| Bankkonto-/kortnummer | ❌ Nej | Aldrig |
|
|
| Lönedata (individnivå) | ❌ Nej | Aldrig |
|
|
| Transaktioner (individnivå, med namn) | ⚠️ Begränsat | Kräver pseudonymisering |
|
|
| Audit-loggar med user_id | ⚠️ Begränsat | Pseudonymisera user_id |
|
|
|
|
**Obligatorisk pseudonymisering:**
|
|
|
|
```javascript
|
|
function pseudonymizeForPrompt(data, tenantId) {
|
|
const piiFields = ['national_id', 'email', 'phone', 'bank_account'];
|
|
|
|
return mapDeep(data, (key, value) => {
|
|
if (piiFields.includes(key)) {
|
|
// Deterministisk pseudonymisering: kan reverseras av ägartenant, ej av AI-lager
|
|
return `[REDACTED:${hashForTenant(value, tenantId).slice(0, 8)}]`;
|
|
}
|
|
return value;
|
|
});
|
|
}
|
|
```
|
|
|
|
**LLM-provider-krav:**
|
|
- Ingen träning på kunddata (kräver `training: false` i API-avtal eller zero-data-retention)
|
|
- EU-baserad databehandling för `restricted`-data (GDPR Art. 44)
|
|
- Promptloggar lagras max 30 dagar och klassas som `confidential`
|
|
|
|
---
|
|
|
|
### 4. Cross-tenant-skydd
|
|
|
|
#### 4.1 Vad händer vid felaktig tenant_id
|
|
|
|
**Applikationslager:**
|
|
```
|
|
1. Middleware validerar tenant_id-format (UUID v4/v7)
|
|
2. Middleware verifierar att tenant existerar och är active
|
|
3. Middleware verifierar att inloggad användare tillhör tenanten
|
|
4. Om något steg misslyckas: 403 Forbidden (aldrig 404 — avslöjar ej existens)
|
|
```
|
|
|
|
**Databaslager (RLS):**
|
|
```
|
|
Om app-lager missar och fel tenant_id når DB:
|
|
→ RLS returnerar 0 rader för SELECT
|
|
→ RLS returnerar 0 rows affected för UPDATE/DELETE
|
|
→ RLS kastar constraint-fel för INSERT med fel tenant_id
|
|
```
|
|
|
|
**Observabilitet:**
|
|
Varje 403-svar med `TENANT_MISMATCH` loggas med:
|
|
- Begärd tenant_id
|
|
- Autentiserad users tenant_id
|
|
- Request path + method
|
|
- IP-adress
|
|
- Timestamp
|
|
|
|
#### 4.2 Audit-trail för cross-tenant-försök
|
|
|
|
```sql
|
|
CREATE TABLE security_audit_log (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
event_type text NOT NULL, -- 'CROSS_TENANT_ATTEMPT', 'RLS_VIOLATION', etc.
|
|
severity text NOT NULL, -- 'warning', 'critical'
|
|
tenant_id UUID, -- Begärd tenant (kan vara ogiltig)
|
|
actor_id UUID, -- Autentiserad principal
|
|
resource text, -- Vad man försökte nå
|
|
details jsonb, -- Full kontext
|
|
ip_address inet,
|
|
created_at timestamptz DEFAULT now()
|
|
);
|
|
|
|
-- Index för snabb sökning per tenant
|
|
CREATE INDEX security_audit_tenant_idx ON security_audit_log (tenant_id, created_at DESC);
|
|
CREATE INDEX security_audit_type_idx ON security_audit_log (event_type, created_at DESC);
|
|
```
|
|
|
|
**Alerting-regler:**
|
|
- `CROSS_TENANT_ATTEMPT`: alert inom 5 minuter (Slack + email till security)
|
|
- 3+ CROSS_TENANT_ATTEMPT från samma IP inom 10 minuter: automatisk IP-block + PagerDuty
|
|
- RLS-violation (data nådde DB-lagret felaktigt): omedelbar incident
|
|
|
|
---
|
|
|
|
### 5. Enforcement — nuläge och väg framåt
|
|
|
|
#### 5.1 Applikationsnivå (dagens approach)
|
|
|
|
```
|
|
Request → Auth middleware → Tenant validation → RLS context set → Business logic → DB
|
|
↑ ↑ ↑
|
|
Lager 1: Lager 2: Lager 3:
|
|
JWT-validering Tenant-existens app.current_tenant_id
|
|
+ user-tenant + user-membership = security context
|
|
association check för RLS
|
|
```
|
|
|
|
**Middleware-ordning (Express):**
|
|
```javascript
|
|
app.use(parseJWT); // 1. Parse + verify JWT
|
|
app.use(extractTenantId); // 2. tenant_id från JWT claims
|
|
app.use(validateTenantActive); // 3. Kontrollera tenant-status
|
|
app.use(assertUserBelongsToTenant); // 4. User ↔ tenant-koppling
|
|
app.use(setDatabaseTenantContext); // 5. Sätt RLS-kontext
|
|
```
|
|
|
|
#### 5.2 DB-level RLS (nästa steg)
|
|
|
|
**Implementationsplan:**
|
|
|
|
```sql
|
|
-- Steg 1: Skapa app-roll med begränsad åtkomst
|
|
CREATE ROLE aamos_app LOGIN PASSWORD '...';
|
|
GRANT CONNECT ON DATABASE aamos TO aamos_app;
|
|
GRANT USAGE ON SCHEMA ekonomi TO aamos_app;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ekonomi TO aamos_app;
|
|
|
|
-- OBS: aamos_app har INTE BYPASS RLS — RLS gäller fullt ut
|
|
|
|
-- Steg 2: Skapa admin-roll (för migrationer) som är undantagen RLS
|
|
CREATE ROLE aamos_admin LOGIN PASSWORD '...';
|
|
ALTER ROLE aamos_admin BYPASSRLS; -- Bara för schema-migrationer
|
|
|
|
-- Steg 3: Verifiera att aamos_app inte kan se cross-tenant data
|
|
SET ROLE aamos_app;
|
|
SELECT set_config('app.current_tenant_id', 'tenant-a-uuid', true);
|
|
-- SELECT FROM transactions WHERE tenant_id = 'tenant-b-uuid' → 0 rows (RLS)
|
|
```
|
|
|
|
**Verifieringstest (körs i CI/CD):**
|
|
|
|
```sql
|
|
-- Automatiserat test: cross-tenant-åtkomst ska returnera 0 rader
|
|
DO $$
|
|
DECLARE
|
|
row_count integer;
|
|
BEGIN
|
|
PERFORM set_config('app.current_tenant_id', 'aaaaaaaa-0000-0000-0000-000000000000', true);
|
|
SELECT COUNT(*) INTO row_count
|
|
FROM ekonomi.transactions
|
|
WHERE tenant_id = 'bbbbbbbb-0000-0000-0000-000000000000';
|
|
|
|
ASSERT row_count = 0, 'RLS FAILURE: Cross-tenant data visible!';
|
|
END;
|
|
$$;
|
|
```
|
|
|
|
---
|
|
|
|
### 6. Tenant-provisioning-spec
|
|
|
|
```javascript
|
|
async function provisionTenant(tenantSpec) {
|
|
const tenantId = uuidv7();
|
|
|
|
await db.transaction(async (trx) => {
|
|
// 1. Skapa tenant-rad
|
|
await trx.insert('tenants', {
|
|
id: tenantId,
|
|
name: tenantSpec.name,
|
|
status: 'active',
|
|
plan: tenantSpec.plan,
|
|
created_at: new Date()
|
|
});
|
|
|
|
// 2. Skapa root Organization
|
|
await trx.insert('organizations', {
|
|
id: uuidv7(),
|
|
tenant_id: tenantId,
|
|
name: tenantSpec.name,
|
|
org_type: 'company',
|
|
status: 'active'
|
|
});
|
|
|
|
// 3. Initiera Redis-namespace (warm up)
|
|
await redis.set(`${tenantId}:meta:provisioned_at`, Date.now());
|
|
|
|
// 4. Publicera provisioning-event på Hermes
|
|
await hermes.publish(`${tenantId}:aamos.platform.tenant.provisioned`, {
|
|
envelope: { tenant_id: tenantId, event_type: 'aamos.platform.tenant.provisioned' },
|
|
payload: { tenant_id: tenantId, name: tenantSpec.name }
|
|
});
|
|
});
|
|
|
|
return tenantId;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Konsekvenser
|
|
|
|
### Positiva
|
|
- **Defense in depth:** Tre oberoende lager (app, RLS, event-envelope) måste alla misslyckas simultant för ett läckage
|
|
- **Revision-ready:** Fullständig audit-trail för alla cross-tenant-försök
|
|
- **GDPR-kompabilitet:** Explicit dataklassificering och AI-prompt-regler gör GDPR-redovisning möjlig
|
|
- **Skalbarhet:** Key-prefix-schema och RLS skalas utan arkitekturförändring
|
|
|
|
### Negativa / risker
|
|
- **RLS-overhead:** `set_config` per anrop kostar ~0.1ms — acceptabelt, men bör mätas vid hög load
|
|
- **Migrationsrisk:** Befintliga tabeller utan `tenant_id` kräver datamigration
|
|
- **Connection pooling:** `set_config` med `is_local=true` är transaction-scoped. Med connection pools MÅSTE tenant-kontext sättas om vid varje transaktion, ej bara vid connection-acquire
|
|
- **Testdisciplin:** CI/CD-tester måste inkludera cross-tenant-assertioner
|
|
|
|
---
|
|
|
|
## Implementation
|
|
|
|
### Fas 1 — Omedelbart (Ekonomimodulen)
|
|
1. Verifiera att alla tabeller har `tenant_id NOT NULL`
|
|
2. Lägg till Express-middleware-kedjan (5 steg ovan)
|
|
3. Skapa `security_audit_log`-tabell
|
|
4. Aktivera RLS på alla ekonomitabeller
|
|
|
|
### Fas 2 — Nästa sprint
|
|
1. Automatiserade cross-tenant-tester i CI
|
|
2. Redis TenantRedisClient-wrapper
|
|
3. Hermes consumer-validering formaliserad
|
|
4. Alerting för CROSS_TENANT_ATTEMPT
|
|
|
|
### Fas 3 — Inför CRM-modul
|
|
1. Dela tenant-infrastruktur mellan moduler (gemensamt `tenants`-schema)
|
|
2. Tenant-provisioning-API
|
|
3. Vector search namespacing implementerat
|
|
4. AI-prompt-pseudonymisering i capability-lager (ARC-002)
|
|
|
|
---
|
|
|
|
## Öppna frågor
|
|
|
|
1. **Multi-tenant vs single-tenant hosting per kund?**
|
|
Nuvarande arkitektur: shared DB med RLS. Vid enterprise-kunder kan dedikerad DB-instans per tenant vara krav.
|
|
|
|
2. **Tenant-merge (företagsförvärv)?**
|
|
Hur hanteras sammanslagning av två tenants? Kräver separata migrationsstrategi och kryptonyckel-hantering.
|
|
|
|
3. **Krypteringsnycklar per tenant?**
|
|
`restricted`-data bör krypteras med tenant-specifika nycklar. Key Management Service (KMS) behöver definieras.
|
|
|
|
4. **Data residency?**
|
|
Vid EU-expansion: måste tenant-data garanteras stanna inom specifik AWS-region. Kräver taggning per tenant.
|