Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Implements secure user credential storage with comprehensive RLS policies and encryption-ready architecture for Phase 3 of M9-CredentialSecurity. **Features:** - UserCredential Prisma model with 19 fields - CredentialType enum (6 values: API_KEY, OAUTH_TOKEN, etc.) - CredentialScope enum (USER, WORKSPACE, SYSTEM) - FORCE ROW LEVEL SECURITY with 3 policies - Encrypted value storage (OpenBao Transit ready) - Cascade delete on user/workspace deletion - Activity logging integration (CREDENTIAL_* actions) - 28 comprehensive test cases **Security:** - RLS owner bypass, user access, workspace admin policies - SQL injection hardening for is_workspace_admin() - Encryption version tracking ready - Full down migration for reversibility **Testing:** - 100% enum coverage (all CredentialType + CredentialScope values) - Unique constraint enforcement - Foreign key cascade deletes - Timestamp behavior validation - JSONB metadata storage **Files:** - Migration: 20260207_add_user_credentials (184 lines + 76 line down.sql) - Security: 20260207163740_fix_sql_injection_is_workspace_admin - Tests: user-credential.model.spec.ts (28 tests, 544 lines) - Docs: README.md (228 lines), scratchpad Fixes #355 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
7.5 KiB
PL/PgSQL
185 lines
7.5 KiB
PL/PgSQL
-- User Credentials Storage with RLS Policies
|
|
-- This migration adds the user_credentials table for secure storage of user API keys,
|
|
-- OAuth tokens, and other credentials with encryption and RLS enforcement.
|
|
--
|
|
-- Related: #355 - Create UserCredential Prisma model with RLS policies
|
|
-- Design: docs/design/credential-security.md (Phase 3a)
|
|
|
|
-- =============================================================================
|
|
-- CREATE ENUMS
|
|
-- =============================================================================
|
|
|
|
-- CredentialType enum: Types of credentials that can be stored
|
|
CREATE TYPE "CredentialType" AS ENUM ('API_KEY', 'OAUTH_TOKEN', 'ACCESS_TOKEN', 'SECRET', 'PASSWORD', 'CUSTOM');
|
|
|
|
-- CredentialScope enum: Access scope for credentials
|
|
CREATE TYPE "CredentialScope" AS ENUM ('USER', 'WORKSPACE', 'SYSTEM');
|
|
|
|
-- =============================================================================
|
|
-- EXTEND EXISTING ENUMS
|
|
-- =============================================================================
|
|
|
|
-- Add CREDENTIAL to EntityType for activity logging
|
|
ALTER TYPE "EntityType" ADD VALUE 'CREDENTIAL';
|
|
|
|
-- Add credential-related actions to ActivityAction
|
|
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_CREATED';
|
|
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_ACCESSED';
|
|
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_ROTATED';
|
|
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_REVOKED';
|
|
|
|
-- =============================================================================
|
|
-- CREATE USER_CREDENTIALS TABLE
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE "user_credentials" (
|
|
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
|
|
"user_id" UUID NOT NULL,
|
|
"workspace_id" UUID,
|
|
|
|
-- Identity
|
|
"name" VARCHAR(255) NOT NULL,
|
|
"provider" VARCHAR(100) NOT NULL,
|
|
"type" "CredentialType" NOT NULL,
|
|
"scope" "CredentialScope" NOT NULL DEFAULT 'USER',
|
|
|
|
-- Encrypted storage
|
|
"encrypted_value" TEXT NOT NULL,
|
|
"masked_value" VARCHAR(20),
|
|
|
|
-- Metadata
|
|
"description" TEXT,
|
|
"expires_at" TIMESTAMPTZ,
|
|
"last_used_at" TIMESTAMPTZ,
|
|
"metadata" JSONB NOT NULL DEFAULT '{}',
|
|
|
|
-- Status
|
|
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
|
"rotated_at" TIMESTAMPTZ,
|
|
|
|
-- Audit
|
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT "user_credentials_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- CREATE FOREIGN KEY CONSTRAINTS
|
|
-- =============================================================================
|
|
|
|
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_user_id_fkey"
|
|
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_workspace_id_fkey"
|
|
FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- =============================================================================
|
|
-- CREATE INDEXES
|
|
-- =============================================================================
|
|
|
|
-- Index for user lookups
|
|
CREATE INDEX "user_credentials_user_id_idx" ON "user_credentials"("user_id");
|
|
|
|
-- Index for workspace lookups
|
|
CREATE INDEX "user_credentials_workspace_id_idx" ON "user_credentials"("workspace_id");
|
|
|
|
-- Index for user + scope queries
|
|
CREATE INDEX "user_credentials_user_id_scope_idx" ON "user_credentials"("user_id", "scope");
|
|
|
|
-- Index for workspace + scope queries
|
|
CREATE INDEX "user_credentials_workspace_id_scope_idx" ON "user_credentials"("workspace_id", "scope");
|
|
|
|
-- Index for scope + active status queries
|
|
CREATE INDEX "user_credentials_scope_is_active_idx" ON "user_credentials"("scope", "is_active");
|
|
|
|
-- =============================================================================
|
|
-- CREATE UNIQUE CONSTRAINT
|
|
-- =============================================================================
|
|
|
|
-- Prevent duplicate credentials per user/workspace/provider/name
|
|
CREATE UNIQUE INDEX "user_credentials_user_id_workspace_id_provider_name_key"
|
|
ON "user_credentials"("user_id", "workspace_id", "provider", "name");
|
|
|
|
-- =============================================================================
|
|
-- ENABLE FORCE ROW LEVEL SECURITY
|
|
-- =============================================================================
|
|
-- FORCE means the table owner (mosaic) is also subject to RLS policies.
|
|
-- This prevents Prisma (connecting as owner) from bypassing policies.
|
|
|
|
ALTER TABLE user_credentials ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE user_credentials FORCE ROW LEVEL SECURITY;
|
|
|
|
-- =============================================================================
|
|
-- RLS POLICIES
|
|
-- =============================================================================
|
|
|
|
-- Owner bypass policy: Allow access to all rows ONLY when no RLS context is set
|
|
-- This is required for:
|
|
-- 1. Prisma migrations that run without RLS context
|
|
-- 2. Database maintenance operations
|
|
-- When RLS context IS set (current_user_id() returns non-NULL), this policy does not apply
|
|
--
|
|
-- NOTE: If connecting as a PostgreSQL superuser (like the default 'mosaic' role),
|
|
-- RLS policies are bypassed entirely. For full RLS enforcement, the application
|
|
-- should connect as a non-superuser role. See docs/design/credential-security.md
|
|
CREATE POLICY user_credentials_owner_bypass ON user_credentials
|
|
FOR ALL
|
|
USING (current_user_id() IS NULL);
|
|
|
|
-- User access policy: USER-scoped credentials visible only to owner
|
|
-- Uses current_user_id() helper from migration 20260129221004_add_rls_policies
|
|
CREATE POLICY user_credentials_user_access ON user_credentials
|
|
FOR ALL
|
|
USING (
|
|
scope = 'USER' AND user_id = current_user_id()
|
|
);
|
|
|
|
-- Workspace admin access policy: WORKSPACE-scoped credentials visible to workspace admins
|
|
-- Uses is_workspace_admin() helper from migration 20260129221004_add_rls_policies
|
|
CREATE POLICY user_credentials_workspace_access ON user_credentials
|
|
FOR ALL
|
|
USING (
|
|
scope = 'WORKSPACE'
|
|
AND workspace_id IS NOT NULL
|
|
AND is_workspace_admin(workspace_id, current_user_id())
|
|
);
|
|
|
|
-- SYSTEM-scoped credentials are only accessible via owner bypass policy
|
|
-- (when current_user_id() IS NULL, which happens for admin operations)
|
|
|
|
-- =============================================================================
|
|
-- AUDIT TRIGGER
|
|
-- =============================================================================
|
|
|
|
-- Update updated_at timestamp on row changes
|
|
CREATE OR REPLACE FUNCTION update_user_credentials_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER user_credentials_updated_at
|
|
BEFORE UPDATE ON user_credentials
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_user_credentials_updated_at();
|
|
|
|
-- =============================================================================
|
|
-- NOTES
|
|
-- =============================================================================
|
|
-- This migration creates the foundation for secure credential storage.
|
|
-- The encrypted_value column stores ciphertext in one of two formats:
|
|
--
|
|
-- 1. OpenBao Transit format (preferred): vault:v1:base64data
|
|
-- 2. AES-256-GCM fallback format: iv:authTag:encrypted
|
|
--
|
|
-- The VaultService (issue #353) handles encryption/decryption with automatic
|
|
-- fallback to CryptoService when OpenBao is unavailable.
|
|
--
|
|
-- RLS enforcement ensures:
|
|
-- - USER scope: Only the credential owner can access
|
|
-- - WORKSPACE scope: Only workspace admins can access
|
|
-- - SYSTEM scope: Only accessible via admin/migration bypass
|