95 lines
3.1 KiB
PL/PgSQL
95 lines
3.1 KiB
PL/PgSQL
-- Extensions
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- For additional crypto if needed
|
|
|
|
-- Schema: api (exposed to PostgREST)
|
|
CREATE SCHEMA api;
|
|
|
|
-- Users Table
|
|
CREATE TABLE api.users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email TEXT UNIQUE NOT NULL CHECK (email ~* '^.+@.+\..+$'),
|
|
verifier TEXT NOT NULL, -- SRP-6a verifier
|
|
salt TEXT NOT NULL,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Secrets Table
|
|
CREATE TABLE api.secrets (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
owner_id UUID NOT NULL REFERENCES api.users(id),
|
|
encrypted_data BYTEA NOT NULL,
|
|
iv TEXT NOT NULL,
|
|
auth_tag TEXT NOT NULL,
|
|
type TEXT DEFAULT 'manual',
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Access Policies (Many-to-Many for sharing)
|
|
CREATE TABLE api.access_policies (
|
|
secret_id UUID NOT NULL REFERENCES api.secrets(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES api.users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
PRIMARY KEY (secret_id, user_id)
|
|
);
|
|
|
|
-- Roles & Permissions (PostgREST)
|
|
-- We need a role for the anonymous web user and one for authenticated users.
|
|
-- Note: 'web_anon' and 'todo_user' are placeholders. We should use:
|
|
-- 'web_anon' (unauthenticated)
|
|
-- 'authenticated' (logged in via JWT)
|
|
|
|
CREATE ROLE web_anon NOLOGIN;
|
|
GRANT USAGE ON SCHEMA api TO web_anon;
|
|
GRANT SELECT ON api.users TO web_anon; -- Needed for login (salt lookup)
|
|
|
|
CREATE ROLE authenticated NOLOGIN;
|
|
GRANT USAGE ON SCHEMA api TO authenticated;
|
|
GRANT ALL ON api.secrets TO authenticated;
|
|
GRANT SELECT ON api.access_policies TO authenticated;
|
|
GRANT SELECT ON api.users TO authenticated;
|
|
|
|
-- Row Level Security (RLS)
|
|
ALTER TABLE api.secrets ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Helper function to get current user ID from JWT
|
|
CREATE OR REPLACE FUNCTION api.uid() RETURNS UUID AS $$
|
|
SELECT NULLIF(current_setting('request.jwt.claim.sub', true), '')::uuid;
|
|
$$ LANGUAGE SQL STABLE;
|
|
|
|
-- RLS Policy for Secrets
|
|
CREATE POLICY secrets_owner_policy ON api.secrets
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (
|
|
owner_id = api.uid()
|
|
)
|
|
WITH CHECK (
|
|
owner_id = api.uid()
|
|
);
|
|
|
|
CREATE POLICY secrets_shared_policy ON api.secrets
|
|
FOR SELECT
|
|
TO authenticated
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM api.access_policies
|
|
WHERE secret_id = api.secrets.id
|
|
AND user_id = api.uid()
|
|
)
|
|
);
|
|
|
|
-- Grant privileges to the authenticator role (the one PostgREST connects as)
|
|
-- In the docker-compose, we used user:pass. We need to grant role usage to 'user'.
|
|
-- Ideally, create a separate 'authenticator' role.
|
|
CREATE ROLE authenticator NOINHERIT LOGIN PASSWORD 'mysecretpassword';
|
|
GRANT web_anon TO authenticator;
|
|
GRANT authenticated TO authenticator;
|
|
|
|
-- BUT since we are using 'user' from the postgres docker image as the connector,
|
|
-- we'll just grant to 'user' or 'postgres'.
|
|
-- Simpler for dev: Grant to the user specified in connection string.
|
|
GRANT web_anon TO "user";
|
|
GRANT authenticated TO "user";
|