fbc034a280
C-3 · Dataskydd genom krypto-shredding. Identifierande fält krypteras med en nyckel per ärende; radering sker genom att nyckeln förstörs. Loggen förblir intakt och hashverifierbar — det som blir oåtkomligt är identifieringen, inte protokollet över vad som kontrollerades. Ett raderat ärende visar fortfarande att lufttrycket mättes till 2,4 bar klockan 08:42, bara inte längre vems bil det gällde. Vad som inte krypteras är lika viktigt: mätvärden, observationer och kontrollresultat är verksamhetsdata. Krypteras allt raderas beviset tillsammans med personuppgiften. En raderingsbegäran gäller ett fordon, inte ett ärende — men identifieraren är krypterad och går inte att söka på. Därför ett blindat index: HMAC av den normaliserade identifieraren, samma fordon ger alltid samma värde, värdet går inte att vända tillbaka utan nyckeln. Gallringsdatum sätts vid avslut utifrån ärendetypen. Ett ärende utan datum gallras aldrig — för tidig gallring går inte att ångra. C-4 · Modellanropen kan stängas av per organisation. Flaggan bärs i token så orkestern kan neka utan databasåtkomst. Metodikmotorn fungerar ensam; en verkstad som inte kan acceptera överföringen till modelleverantören kan ändå använda produkten. M-1 · Mätvärden bär vilket mätdon som användes och när det var kalibrerat. Utan spårbart instrument nedgraderas värdet från E4 till E1 — det är teknikerns observation av en siffra, inte en mätning. Kalibreringen bedöms vid mättillfället, inte i dag. Demoärendet fick kalibrerade instrument: det ska visa den praxis produkten kräver. M-4 · Läslogg. Varje skrivning loggades redan; ingen läsning gjorde det. M-5 · Återställningstest i CI. Larmet visade att backup sker, inte att den går att återställa. Testet kontrollerar det som faktiskt brukar tappas: att append-only-triggarna följde med och fortfarande biter. M-6 · Regelpaketet verifieras mot HMAC. Ogiltig signatur spärrar avslut; saknad nyckel ger granskningsläge i stället för driftavbrott — det är så säkerhetsfunktioner blir avstängda. m-3 · EXIF-borttagningen är nu avsiktlig och låst av test. Den höll av en slump, och en ändring i stil med "bevara originalkvaliteten" hade tyst börjat publicera var kundens bil stod. m-5 · SBOM och sårbarhetsskanning i CI. m-7 · Promptversionen binds till varje svar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
235 lines
9.9 KiB
PL/PgSQL
235 lines
9.9 KiB
PL/PgSQL
-- Guidad Felsökning: schema för den självhostade plattformen.
|
|
-- Multi-tenant: varje organisation är en egen tenant — all ärendedata är
|
|
-- organisationsknuten och API:t släpper aldrig data över organisationsgränsen.
|
|
-- Append-only garanteras med triggers: historik kan inte ändras eller
|
|
-- raderas oavsett vilken roll som ansluter.
|
|
|
|
create extension if not exists pgcrypto;
|
|
|
|
create table if not exists organisationer (
|
|
id uuid primary key default gen_random_uuid(),
|
|
namn text not null,
|
|
-- Vad som visas när ett ärende startas (objekttyper,
|
|
-- identifieringsmetoder) — sätts av systemadministratören.
|
|
installningar jsonb not null default '{}'::jsonb,
|
|
skapad timestamptz not null default now()
|
|
);
|
|
alter table organisationer add column if not exists installningar jsonb not null default '{}'::jsonb;
|
|
|
|
-- Märkesspecifika kopplingar per organisation. Uppgifterna lagras
|
|
-- krypterade (AES-256-GCM, nyckel ur INTEGRATION_NYCKEL) och lämnar
|
|
-- aldrig servern i klartext — klienten ser bara maskerade värden.
|
|
create table if not exists integrationer (
|
|
organisation_id uuid not null references organisationer(id),
|
|
leverantor text not null,
|
|
uppgifter_krypt text not null,
|
|
aktiv boolean not null default true,
|
|
skapad timestamptz not null default now(),
|
|
uppdaterad timestamptz not null default now(),
|
|
senast_testad timestamptz,
|
|
senaste_status text,
|
|
primary key (organisation_id, leverantor)
|
|
);
|
|
|
|
create table if not exists anvandare (
|
|
id uuid primary key default gen_random_uuid(),
|
|
organisation_id uuid not null references organisationer(id),
|
|
epost text unique not null,
|
|
losen_hash text not null,
|
|
namn text not null,
|
|
roll text not null default 'tekniker'
|
|
check (roll in ('tekniker', 'arbetsledare', 'admin')),
|
|
-- En avaktiverad användare kan varken logga in eller använda en token
|
|
-- som redan är utfärdad.
|
|
aktiv boolean not null default true,
|
|
-- Räknare för återkallelse. Utfärdade tokens bär sitt värde; höjs det
|
|
-- slutar alla tidigare utfärdade att gälla omedelbart. Loggen rörs
|
|
-- inte — historiken är fortfarande knuten till personen.
|
|
token_version integer not null default 0,
|
|
skapad timestamptz not null default now()
|
|
);
|
|
alter table anvandare add column if not exists aktiv boolean not null default true;
|
|
alter table anvandare add column if not exists token_version integer not null default 0;
|
|
|
|
-- Inloggningsförsök: underlag för takt-begränsning som fungerar bakom
|
|
-- flera repliker (den i minnet gör det inte). Inget lösenord lagras —
|
|
-- bara att ett försök skedde och om det lyckades.
|
|
create table if not exists inloggningsforsok (
|
|
id bigserial primary key,
|
|
epost text not null,
|
|
kalla text not null default '',
|
|
lyckades boolean not null,
|
|
tidpunkt timestamptz not null default now()
|
|
);
|
|
create index if not exists inloggningsforsok_epost_idx
|
|
on inloggningsforsok (epost, tidpunkt desc);
|
|
create index if not exists inloggningsforsok_kalla_idx
|
|
on inloggningsforsok (kalla, tidpunkt desc);
|
|
|
|
create table if not exists felsokning_arenden (
|
|
id text primary key,
|
|
organisation_id uuid not null references organisationer(id),
|
|
nummer integer not null,
|
|
skapad timestamptz not null,
|
|
delningskod text unique,
|
|
metodik_id text,
|
|
skapad_av uuid references anvandare(id),
|
|
insatt timestamptz not null default now()
|
|
);
|
|
create index if not exists felsokning_arenden_org_idx
|
|
on felsokning_arenden (organisation_id, skapad desc);
|
|
|
|
-- Bilagor: foton, video och instrumentbilder. Innehållet ligger utanför
|
|
-- händelsen; loggen bär en referens och innehållets SHA-256. Hashen står
|
|
-- i den append-only-skyddade loggen, så en utbytt bild går att upptäcka —
|
|
-- starkare bevisvärde än när bilden låg inbäddad och måste tros på.
|
|
create table if not exists bilagor (
|
|
id text primary key,
|
|
organisation_id uuid not null references organisationer(id),
|
|
arende_id text not null,
|
|
hash text not null,
|
|
mediatyp text not null,
|
|
storlek integer not null,
|
|
laddad_av uuid references anvandare(id),
|
|
skapad timestamptz not null default now()
|
|
);
|
|
create index if not exists bilagor_arende_idx on bilagor (arende_id);
|
|
create index if not exists bilagor_hash_idx on bilagor (hash);
|
|
|
|
-- Själva bytesen när BILAGE_LAGE=databas. Innehållsadresserat: samma
|
|
-- foto som dokumenteras två gånger lagras en gång. I s3-läget är den
|
|
-- här tabellen tom och innehållet ligger i objektlagringen.
|
|
create table if not exists bilage_innehall (
|
|
hash text primary key,
|
|
data bytea not null,
|
|
skapad timestamptz not null default now()
|
|
);
|
|
|
|
create table if not exists felsokning_handelser (
|
|
id text primary key,
|
|
arende_id text not null references felsokning_arenden(id),
|
|
tidpunkt timestamptz not null,
|
|
anvandare text not null,
|
|
handelse jsonb not null,
|
|
insatt timestamptz not null default now()
|
|
);
|
|
create index if not exists felsokning_handelser_arende_idx
|
|
on felsokning_handelser (arende_id, tidpunkt);
|
|
|
|
create or replace function forbjud_andring() returns trigger
|
|
language plpgsql as $$
|
|
begin
|
|
raise exception 'Loggen är append-only — historik kan inte ändras eller raderas';
|
|
end $$;
|
|
|
|
drop trigger if exists handelser_append_only on felsokning_handelser;
|
|
create trigger handelser_append_only
|
|
before update or delete on felsokning_handelser
|
|
for each row execute function forbjud_andring();
|
|
|
|
drop trigger if exists arenden_append_only on felsokning_arenden;
|
|
create trigger arenden_append_only
|
|
before update or delete on felsokning_arenden
|
|
for each row execute function forbjud_andring();
|
|
|
|
-- Live Share-delningar: återkallbara länkar med behörighetsnivå.
|
|
-- (Åtkomststyrning, inte journal — därför ingen append-only-trigger:
|
|
-- återkallelse sätter aterkallad-tidpunkten.)
|
|
create table if not exists delningar (
|
|
kod text primary key,
|
|
arende_id text not null references felsokning_arenden(id),
|
|
niva text not null check (niva in ('kund', 'partner', 'intern')),
|
|
skapad_av uuid references anvandare(id),
|
|
skapad timestamptz not null default now(),
|
|
aterkallad timestamptz
|
|
);
|
|
create index if not exists delningar_arende_idx on delningar (arende_id);
|
|
|
|
-- ---- Personuppgifter och gallring -------------------------------------
|
|
--
|
|
-- Append-only och rätten till radering (dataskyddsförordningen art. 17)
|
|
-- är inte oförenliga, men de måste förenas medvetet. Krypto-shredding:
|
|
-- identifierande fält i loggen krypteras med en nyckel per registrerad,
|
|
-- och radering sker genom att nyckeln förstörs.
|
|
--
|
|
-- Loggen förblir intakt och hashverifierbar. Det som blir oåtkomligt är
|
|
-- identifieringen, inte protokollet över vad som kontrollerades — ett
|
|
-- raderat ärende kan fortfarande visa att lufttrycket mättes till
|
|
-- 2,4 bar klockan 08:42, bara inte längre vems bil det gällde.
|
|
--
|
|
-- Tabellen är det enda stället i schemat där en rad FÅR försvinna, och
|
|
-- det är hela poängen med den.
|
|
create table if not exists personnycklar (
|
|
id uuid primary key default gen_random_uuid(),
|
|
organisation_id uuid not null references organisationer(id),
|
|
-- Vad nyckeln skyddar: normalt ett fordon (regnr/VIN) eller en kund.
|
|
subjekt text not null,
|
|
nyckel bytea not null,
|
|
skapad timestamptz not null default now(),
|
|
-- Sätts när radering begärts men nyckeln ännu inte förstörts, så att
|
|
-- begäran syns i systemet även innan den verkställts.
|
|
radering_begard timestamptz,
|
|
unique (organisation_id, subjekt)
|
|
);
|
|
|
|
-- Radering loggas. Att en begäran verkställts måste gå att visa i
|
|
-- efterhand, och den loggen får själv inte innehålla personuppgiften.
|
|
create table if not exists raderingar (
|
|
id uuid primary key default gen_random_uuid(),
|
|
organisation_id uuid not null references organisationer(id),
|
|
subjekt_hash text not null,
|
|
begard timestamptz not null,
|
|
verkstalld timestamptz not null default now(),
|
|
begard_av text not null,
|
|
antal_arenden integer not null default 0
|
|
);
|
|
|
|
-- Gallringsdatum per ärende, satt vid avslut utifrån ärendetypen.
|
|
-- Ett ärende utan datum gallras aldrig automatiskt — det försiktiga
|
|
-- utfallet, eftersom för tidig gallring inte går att ångra.
|
|
alter table felsokning_arenden add column if not exists gallras_efter timestamptz;
|
|
|
|
-- Läslogg. Varje skrivning loggades redan; ingen läsning gjorde det.
|
|
-- Utan den går det inte att svara på vem som sett en kunds uppgifter,
|
|
-- vilket både artikel 32 och en verkstadschef vill kunna få svar på.
|
|
create table if not exists atkomstlogg (
|
|
id bigserial primary key,
|
|
tidpunkt timestamptz not null default now(),
|
|
organisation_id uuid,
|
|
anvandare_id uuid,
|
|
arende_id text,
|
|
vag text not null,
|
|
kalla text,
|
|
-- Delningskod när åtkomsten skedde via en publik länk.
|
|
delningskod text
|
|
);
|
|
create index if not exists atkomstlogg_arende on atkomstlogg (arende_id, tidpunkt desc);
|
|
create index if not exists atkomstlogg_org on atkomstlogg (organisation_id, tidpunkt desc);
|
|
|
|
-- Mätdon. Ett mätvärde rankas som hög evidens (E4); utan kalibrerat
|
|
-- instrument är det inte lägre evidens utan ingen evidens alls.
|
|
create table if not exists matdon (
|
|
id uuid primary key default gen_random_uuid(),
|
|
organisation_id uuid not null references organisationer(id),
|
|
beteckning text not null,
|
|
serienummer text not null,
|
|
kalibrerad_till date,
|
|
aktiv boolean not null default true,
|
|
skapad timestamptz not null default now(),
|
|
unique (organisation_id, serienummer)
|
|
);
|
|
|
|
-- Blindat index över fordonsidentifieraren.
|
|
--
|
|
-- En raderingsbegäran gäller ett fordon eller en person, inte ett enskilt
|
|
-- ärende — men identifieraren är krypterad, så den går inte att söka på.
|
|
-- Indexet är en HMAC av den normaliserade identifieraren med en
|
|
-- servernyckel: samma fordon ger alltid samma värde, och värdet går inte
|
|
-- att vända tillbaka till ett registreringsnummer utan nyckeln.
|
|
--
|
|
-- Det gör radering över hela fordonets historik möjlig utan att lagra
|
|
-- identifieraren i klartext någonstans.
|
|
alter table felsokning_arenden add column if not exists identifierare_index text;
|
|
create index if not exists felsokning_arenden_ident_idx
|
|
on felsokning_arenden (organisation_id, identifierare_index);
|