mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-04-09 17:36:23 +00:00
* Add ConfigKeyStore for in-memory key management * Introduce DatabaseKeyStore for PostgreSQL-backed key storage * Create KeyStoreAuthenticator for API key validation * Define SQL procedures for key management in PostgreSQL * Document keystore functionality and usage in KEYSTORE.md
188 lines
6.3 KiB
PL/PgSQL
188 lines
6.3 KiB
PL/PgSQL
-- Keystore schema for per-user auth keys
|
|
-- Apply alongside database_schema.sql (requires the users table)
|
|
|
|
CREATE TABLE IF NOT EXISTS user_keys (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
key_type VARCHAR(50) NOT NULL,
|
|
key_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hex digest (64 chars)
|
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
|
scopes TEXT, -- JSON array, e.g. '["read","write"]'
|
|
meta JSONB,
|
|
expires_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
last_used_at TIMESTAMP,
|
|
is_active BOOLEAN DEFAULT true
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_keys_user_id ON user_keys(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_user_keys_key_hash ON user_keys(key_hash);
|
|
CREATE INDEX IF NOT EXISTS idx_user_keys_key_type ON user_keys(key_type);
|
|
|
|
-- resolvespec_keystore_get_user_keys
|
|
-- Returns all active, non-expired keys for a user.
|
|
-- Pass empty p_key_type to return all key types.
|
|
CREATE OR REPLACE FUNCTION resolvespec_keystore_get_user_keys(
|
|
p_user_id INTEGER,
|
|
p_key_type TEXT DEFAULT ''
|
|
)
|
|
RETURNS TABLE(p_success BOOLEAN, p_error TEXT, p_keys JSONB)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
v_keys JSONB;
|
|
BEGIN
|
|
SELECT COALESCE(
|
|
jsonb_agg(
|
|
jsonb_build_object(
|
|
'id', k.id,
|
|
'user_id', k.user_id,
|
|
'key_type', k.key_type,
|
|
'name', k.name,
|
|
'scopes', CASE WHEN k.scopes IS NOT NULL THEN k.scopes::jsonb ELSE '[]'::jsonb END,
|
|
'meta', COALESCE(k.meta, '{}'::jsonb),
|
|
'expires_at', k.expires_at,
|
|
'created_at', k.created_at,
|
|
'last_used_at', k.last_used_at,
|
|
'is_active', k.is_active
|
|
)
|
|
),
|
|
'[]'::jsonb
|
|
)
|
|
INTO v_keys
|
|
FROM user_keys k
|
|
WHERE k.user_id = p_user_id
|
|
AND k.is_active = true
|
|
AND (k.expires_at IS NULL OR k.expires_at > NOW())
|
|
AND (p_key_type = '' OR k.key_type = p_key_type);
|
|
|
|
RETURN QUERY SELECT true, NULL::TEXT, v_keys;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN QUERY SELECT false, SQLERRM, NULL::JSONB;
|
|
END;
|
|
$$;
|
|
|
|
-- resolvespec_keystore_create_key
|
|
-- Inserts a new key row. key_hash is provided by the caller (Go hashes the raw key).
|
|
-- Returns the created key record (without key_hash).
|
|
CREATE OR REPLACE FUNCTION resolvespec_keystore_create_key(
|
|
p_request JSONB
|
|
)
|
|
RETURNS TABLE(p_success BOOLEAN, p_error TEXT, p_key JSONB)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
v_id BIGINT;
|
|
v_created_at TIMESTAMP;
|
|
v_key JSONB;
|
|
BEGIN
|
|
INSERT INTO user_keys (user_id, key_type, key_hash, name, scopes, meta, expires_at)
|
|
VALUES (
|
|
(p_request->>'user_id')::INTEGER,
|
|
p_request->>'key_type',
|
|
p_request->>'key_hash',
|
|
COALESCE(p_request->>'name', ''),
|
|
p_request->>'scopes',
|
|
p_request->'meta',
|
|
CASE WHEN p_request->>'expires_at' IS NOT NULL
|
|
THEN (p_request->>'expires_at')::TIMESTAMP
|
|
ELSE NULL
|
|
END
|
|
)
|
|
RETURNING id, created_at INTO v_id, v_created_at;
|
|
|
|
v_key := jsonb_build_object(
|
|
'id', v_id,
|
|
'user_id', (p_request->>'user_id')::INTEGER,
|
|
'key_type', p_request->>'key_type',
|
|
'name', COALESCE(p_request->>'name', ''),
|
|
'scopes', CASE WHEN p_request->>'scopes' IS NOT NULL
|
|
THEN (p_request->>'scopes')::jsonb
|
|
ELSE '[]'::jsonb END,
|
|
'meta', COALESCE(p_request->'meta', '{}'::jsonb),
|
|
'expires_at', p_request->>'expires_at',
|
|
'created_at', v_created_at,
|
|
'is_active', true
|
|
);
|
|
|
|
RETURN QUERY SELECT true, NULL::TEXT, v_key;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN QUERY SELECT false, SQLERRM, NULL::JSONB;
|
|
END;
|
|
$$;
|
|
|
|
-- resolvespec_keystore_delete_key
|
|
-- Soft-deletes a key (is_active = false) after verifying ownership.
|
|
-- Returns p_key_hash so the caller can invalidate cache entries without a separate query.
|
|
CREATE OR REPLACE FUNCTION resolvespec_keystore_delete_key(
|
|
p_user_id INTEGER,
|
|
p_key_id BIGINT
|
|
)
|
|
RETURNS TABLE(p_success BOOLEAN, p_error TEXT, p_key_hash TEXT)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
v_hash TEXT;
|
|
BEGIN
|
|
UPDATE user_keys
|
|
SET is_active = false
|
|
WHERE id = p_key_id AND user_id = p_user_id AND is_active = true
|
|
RETURNING key_hash INTO v_hash;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN QUERY SELECT false, 'key not found or already deleted'::TEXT, NULL::TEXT;
|
|
RETURN;
|
|
END IF;
|
|
|
|
RETURN QUERY SELECT true, NULL::TEXT, v_hash;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN QUERY SELECT false, SQLERRM, NULL::TEXT;
|
|
END;
|
|
$$;
|
|
|
|
-- resolvespec_keystore_validate_key
|
|
-- Looks up a key by its SHA-256 hash, checks active status and expiry,
|
|
-- updates last_used_at, and returns the key record.
|
|
-- p_key_type can be empty to accept any key type.
|
|
CREATE OR REPLACE FUNCTION resolvespec_keystore_validate_key(
|
|
p_key_hash TEXT,
|
|
p_key_type TEXT DEFAULT ''
|
|
)
|
|
RETURNS TABLE(p_success BOOLEAN, p_error TEXT, p_key JSONB)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
v_key_rec user_keys%ROWTYPE;
|
|
v_key JSONB;
|
|
BEGIN
|
|
SELECT * INTO v_key_rec
|
|
FROM user_keys
|
|
WHERE key_hash = p_key_hash
|
|
AND is_active = true
|
|
AND (expires_at IS NULL OR expires_at > NOW())
|
|
AND (p_key_type = '' OR key_type = p_key_type);
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN QUERY SELECT false, 'invalid or expired key'::TEXT, NULL::JSONB;
|
|
RETURN;
|
|
END IF;
|
|
|
|
UPDATE user_keys SET last_used_at = NOW() WHERE id = v_key_rec.id;
|
|
|
|
v_key := jsonb_build_object(
|
|
'id', v_key_rec.id,
|
|
'user_id', v_key_rec.user_id,
|
|
'key_type', v_key_rec.key_type,
|
|
'name', v_key_rec.name,
|
|
'scopes', CASE WHEN v_key_rec.scopes IS NOT NULL
|
|
THEN v_key_rec.scopes::jsonb
|
|
ELSE '[]'::jsonb END,
|
|
'meta', COALESCE(v_key_rec.meta, '{}'::jsonb),
|
|
'expires_at', v_key_rec.expires_at,
|
|
'created_at', v_key_rec.created_at,
|
|
'last_used_at', NOW(),
|
|
'is_active', v_key_rec.is_active
|
|
);
|
|
|
|
RETURN QUERY SELECT true, NULL::TEXT, v_key;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN QUERY SELECT false, SQLERRM, NULL::JSONB;
|
|
END;
|
|
$$;
|