From 666bc07f62135c3b8b56f7cde57247fddeb4108d Mon Sep 17 00:00:00 2001 From: David Cox Date: Thu, 21 Nov 2024 10:04:11 -0800 Subject: [PATCH] feat(extension): add user impersonation, simplify rbac --- client/tests/tests.sql | 471 ++++------ extension/keyhippo--0.0.40.sql | 1611 +++++++++----------------------- 2 files changed, 622 insertions(+), 1460 deletions(-) diff --git a/client/tests/tests.sql b/client/tests/tests.sql index 4a78987..f04368e 100644 --- a/client/tests/tests.sql +++ b/client/tests/tests.sql @@ -1,5 +1,5 @@ BEGIN; -SET search_path TO keyhippo, public, auth; +SET search_path TO keyhippo, keyhippo_rbac, public, auth; -- Create test users and set up authentication DO $$ DECLARE @@ -8,358 +8,279 @@ DECLARE admin_group_id uuid; admin_role_id uuid; BEGIN - RAISE NOTICE 'Debug: Creating test users'; - -- Switch to a role with elevated privileges to insert users - SET local ROLE postgres; -- Insert users with explicit IDs INSERT INTO auth.users (id, email) VALUES (user1_id, 'user1@example.com'), (user2_id, 'user2@example.com'); - RAISE NOTICE 'Debug: Users created with IDs: % and %', user1_id, user2_id; -- Store user IDs as settings for later use PERFORM set_config('test.user1_id', user1_id::text, TRUE); PERFORM set_config('test.user2_id', user2_id::text, TRUE); - RAISE NOTICE 'Debug: User IDs stored in settings'; - -- Ensure 'Admin Group' exists - INSERT INTO keyhippo_rbac.groups (name, description) - VALUES ('Admin Group', 'Group for administrators') - ON CONFLICT (name) - DO UPDATE SET - description = EXCLUDED.description - RETURNING - id INTO admin_group_id; - RAISE NOTICE 'Debug: Admin Group created/updated with ID: %', admin_group_id; - -- Ensure 'Admin' role exists and is associated with 'Admin Group' - INSERT INTO keyhippo_rbac.roles (name, description, group_id) - VALUES ('Admin', 'Administrator role', admin_group_id) - ON CONFLICT (name, group_id) - DO UPDATE SET - description = EXCLUDED.description - RETURNING - id INTO admin_role_id; - RAISE NOTICE 'Debug: Admin Role created/updated with ID: %', admin_role_id; - -- Ensure 'manage_user_attributes' permission exists - INSERT INTO keyhippo_rbac.permissions (name, description) - VALUES ('manage_user_attributes', 'Permission to manage user attributes') - ON CONFLICT (name) - DO NOTHING; - RAISE NOTICE 'Debug: manage_user_attributes permission created/updated'; - -- Assign 'manage_user_attributes' permission to 'Admin' role - INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id) + -- Initialize KeyHippo (this creates default groups and roles) + PERFORM + keyhippo.initialize_keyhippo (); + -- Get the Admin Group and Role IDs SELECT - admin_role_id, - id + id INTO admin_group_id FROM - keyhippo_rbac.permissions + keyhippo_rbac.groups WHERE - name = 'manage_user_attributes' - ON CONFLICT (role_id, - permission_id) - DO NOTHING; - RAISE NOTICE 'Debug: manage_user_attributes permission assigned to Admin role'; - -- Assign user1 to 'Admin' role - INSERT INTO keyhippo_rbac.user_group_roles (user_id, group_id, role_id) - VALUES (user1_id, admin_group_id, admin_role_id) - ON CONFLICT (user_id, group_id, role_id) - DO NOTHING; - RAISE NOTICE 'Debug: User1 assigned to Admin role'; - -- Update claims cache for user1 - PERFORM - keyhippo_rbac.update_user_claims_cache (user1_id); - RAISE NOTICE 'Debug: Claims cache updated for User1'; - -- Set up authentication context for user1 + name = 'Admin Group'; + SELECT + id INTO admin_role_id + FROM + keyhippo_rbac.roles + WHERE + name = 'Admin' + AND group_id = admin_group_id; + -- Assign admin role to user1 + -- Set up authentication for user1 PERFORM set_config('request.jwt.claim.sub', user1_id::text, TRUE); PERFORM - set_config('request.jwt.claims', json_build_object('sub', user1_id, 'role', 'authenticated')::text, TRUE); - RAISE NOTICE 'Debug: Authentication context set up for User1'; + set_config('request.jwt.claims', json_build_object('sub', user1_id, 'role', 'authenticated', 'user_role', 'admin')::text, TRUE); END $$; --- Log current user and session details +-- Switch to authenticated role +SET ROLE authenticated; +-- Test RBAC initialization DO $$ DECLARE - CURRENT_USER text := CURRENT_USER; - SESSION_USER text := SESSION_USER; - search_path text; + group_count integer; + role_count integer; + permission_count integer; + admin_role_permissions integer; + user_role_permissions integer; BEGIN + RAISE NOTICE 'Current user: %', CURRENT_USER; + RAISE NOTICE 'Current role: %', CURRENT_ROLE; + -- Check groups SELECT - setting INTO search_path + COUNT(*) INTO group_count FROM - pg_settings - WHERE - name = 'search_path'; - RAISE NOTICE 'Debug: Current user: %, Session user: %, Search path: %', CURRENT_USER, SESSION_USER, search_path; -END -$$; --- Fetch and set group and role IDs -DO $$ -DECLARE - admin_group uuid; - user_group uuid; - admin_role uuid; - user_role uuid; -BEGIN - -- Fetch Admin Group ID + keyhippo_rbac.groups; + RAISE NOTICE 'Number of groups: %', group_count; + RAISE NOTICE 'Group names: %', array_agg(name) +FROM + keyhippo_rbac.groups; + -- Check if the current user has permissions to see the groups + RAISE NOTICE 'Can select from groups: %', EXISTS ( + SELECT + 1 + FROM + information_schema.table_privileges + WHERE + table_schema = 'keyhippo_rbac' + AND table_name = 'groups' + AND privilege_type = 'SELECT' + AND grantee = CURRENT_USER); + ASSERT group_count = 2, + 'Two default groups should be created'; SELECT - id INTO admin_group + COUNT(*) INTO role_count FROM - keyhippo_rbac.groups - WHERE - name = 'Admin Group'; - IF NOT FOUND THEN - RAISE EXCEPTION 'Admin Group not found'; - END IF; - RAISE NOTICE 'Debug: Admin Group ID fetched: %', admin_group; - -- Fetch User Group ID + keyhippo_rbac.roles; + ASSERT role_count = 2, + 'Two default roles should be created'; SELECT - id INTO user_group + COUNT(*) INTO permission_count FROM - keyhippo_rbac.groups - WHERE - name = 'User Group'; - IF NOT FOUND THEN - RAISE EXCEPTION 'User Group not found'; - END IF; - RAISE NOTICE 'Debug: User Group ID fetched: %', user_group; - -- Fetch Admin Role ID + keyhippo_rbac.permissions; + ASSERT permission_count = 6, + 'Six default permissions should be created'; SELECT - id INTO admin_role + COUNT(*) INTO admin_role_permissions FROM - keyhippo_rbac.roles + keyhippo_rbac.role_permissions rp + JOIN keyhippo_rbac.roles r ON rp.role_id = r.id WHERE - name = 'Admin' - AND group_id = admin_group; - IF NOT FOUND THEN - RAISE EXCEPTION 'Admin Role not found in Admin Group'; - END IF; - RAISE NOTICE 'Debug: Admin Role ID fetched: %', admin_role; - -- Fetch User Role ID + r.name = 'Admin'; + ASSERT admin_role_permissions = 6, + 'Admin role should have all 6 permissions'; SELECT - id INTO user_role + COUNT(*) INTO user_role_permissions FROM - keyhippo_rbac.roles + keyhippo_rbac.role_permissions rp + JOIN keyhippo_rbac.roles r ON rp.role_id = r.id WHERE - name = 'User' - AND group_id = user_group; - IF NOT FOUND THEN - RAISE EXCEPTION 'User Role not found in User Group'; - END IF; - RAISE NOTICE 'Debug: User Role ID fetched: %', user_role; - -- Set custom configuration parameters - PERFORM - set_config('test.admin_group_id', admin_group::text, TRUE); - PERFORM - set_config('test.user_group_id', user_group::text, TRUE); - PERFORM - set_config('test.admin_role_id', admin_role::text, TRUE); - PERFORM - set_config('test.user_role_id', user_role::text, TRUE); - RAISE NOTICE 'Debug: Group and Role IDs set in configuration'; + r.name = 'User'; + ASSERT user_role_permissions = 1, + 'User role should have 1 permission (manage_api_keys)'; END $$; --- Test 1: Verify initial state (no API keys) +-- Test create_api_key function DO $$ DECLARE + created_key_result record; key_count bigint; BEGIN + SELECT + * INTO created_key_result + FROM + keyhippo.create_api_key ('Test API Key'); + ASSERT created_key_result.api_key IS NOT NULL, + 'create_api_key executes successfully for authenticated user'; + ASSERT created_key_result.api_key_id IS NOT NULL, + 'create_api_key returns a valid API key ID'; SELECT COUNT(*) INTO key_count FROM keyhippo.api_key_metadata WHERE - user_id = current_setting('test.user1_id')::uuid; - ASSERT key_count = 0, - 'Initially, no API keys should exist for the user'; - RAISE NOTICE 'Debug: Test 1 passed - No initial API keys'; + description = 'Test API Key' + AND user_id = current_setting('test.user1_id')::uuid; + ASSERT key_count = 1, + 'An API key should be created with the given name for the authenticated user'; END $$; --- Test 2: Create an API key +-- Test verify_api_key function DO $$ DECLARE created_key_result record; - key_count bigint; + verified_key_result record; BEGIN SELECT * INTO created_key_result FROM - keyhippo.create_api_key ('Test API Key'); - ASSERT created_key_result.api_key IS NOT NULL, - 'create_api_key should return a valid API key'; - ASSERT created_key_result.api_key_id IS NOT NULL, - 'create_api_key should return a valid API key ID'; + keyhippo.create_api_key ('Verify Test Key'); SELECT - COUNT(*) INTO key_count + * INTO verified_key_result + FROM + keyhippo.verify_api_key (created_key_result.api_key); + ASSERT verified_key_result.user_id = current_setting('test.user1_id')::uuid, + 'verify_api_key should return the correct user_id'; + ASSERT verified_key_result.scope_id IS NULL, + 'verify_api_key should return NULL scope_id for default key'; + ASSERT array_length(verified_key_result.permissions, 1) > 0, + 'verify_api_key should return permissions'; +END +$$; +-- Test revoke_api_key function +DO $$ +DECLARE + created_key_result record; + revoke_result boolean; + key_is_revoked boolean; +BEGIN + SELECT + * INTO created_key_result + FROM + keyhippo.create_api_key ('Revoke Test Key'); + SELECT + * INTO revoke_result + FROM + keyhippo.revoke_api_key (created_key_result.api_key_id); + ASSERT revoke_result = TRUE, + 'revoke_api_key should return TRUE for successful revocation'; + SELECT + is_revoked INTO key_is_revoked FROM keyhippo.api_key_metadata WHERE - id = created_key_result.api_key_id - AND user_id = current_setting('test.user1_id')::uuid; - ASSERT key_count = 1, - 'An API key should be created for the authenticated user'; - RAISE NOTICE 'Debug: Test 2 passed - API key created successfully'; + id = created_key_result.api_key_id; + ASSERT key_is_revoked = TRUE, + 'API key should be marked as revoked'; END $$; --- Continue with the rest of the tests, adding RAISE NOTICE statements for debugging... --- Test 25: keyhippo.is_authorized function test +-- Test rotate_api_key function DO $$ DECLARE - v_admin_group_id uuid; - v_admin_role_id uuid; - v_admin_user_id uuid; - v_manage_users_permission_id uuid; - v_regular_user_id uuid; - v_test_table_id oid; - v_user_group_id uuid; - v_user_role_id uuid; - v_view_reports_permission_id uuid; + created_key_result record; + rotated_key_result record; + old_key_revoked boolean; BEGIN - RAISE NOTICE 'Debug: Starting test setup'; - -- Create test users - INSERT INTO auth.users (id, email) - VALUES (gen_random_uuid (), 'admin@example.com') - RETURNING - id INTO v_admin_user_id; - RAISE NOTICE 'Debug: Admin user created with ID: %', v_admin_user_id; - INSERT INTO auth.users (id, email) - VALUES (gen_random_uuid (), 'user@example.com') - RETURNING - id INTO v_regular_user_id; - RAISE NOTICE 'Debug: Regular user created with ID: %', v_regular_user_id; - -- Insert groups if they don't exist - INSERT INTO keyhippo_rbac.groups (name) - VALUES ('Admin Group'), - ('User Group') - ON CONFLICT (name) - DO NOTHING; - RAISE NOTICE 'Debug: Groups inserted'; - -- Retrieve group IDs SELECT - id INTO v_admin_group_id + * INTO created_key_result FROM - keyhippo_rbac.groups - WHERE - name = 'Admin Group'; - RAISE NOTICE 'Debug: Admin group ID: %', v_admin_group_id; + keyhippo.create_api_key ('Rotate Test Key'); SELECT - id INTO v_user_group_id + * INTO rotated_key_result FROM - keyhippo_rbac.groups - WHERE - name = 'User Group'; - RAISE NOTICE 'Debug: User group ID: %', v_user_group_id; - -- Insert roles if they don't exist - INSERT INTO keyhippo_rbac.roles (name, group_id) - VALUES ('Admin', v_admin_group_id), - ('User', v_user_group_id) - ON CONFLICT (name, group_id) - DO NOTHING; - RAISE NOTICE 'Debug: Roles inserted'; - -- Retrieve role IDs + keyhippo.rotate_api_key (created_key_result.api_key_id); + ASSERT rotated_key_result.new_api_key IS NOT NULL, + 'rotate_api_key should return a new API key'; + ASSERT rotated_key_result.new_api_key_id IS NOT NULL, + 'rotate_api_key should return a new API key ID'; + ASSERT rotated_key_result.new_api_key_id != created_key_result.api_key_id, + 'New API key ID should be different from the old one'; SELECT - id INTO v_admin_role_id + is_revoked INTO old_key_revoked FROM - keyhippo_rbac.roles + keyhippo.api_key_metadata WHERE - name = 'Admin' - AND group_id = v_admin_group_id; - RAISE NOTICE 'Debug: Admin role ID: %', v_admin_role_id; + id = created_key_result.api_key_id; + ASSERT old_key_revoked = TRUE, + 'Old API key should be revoked after rotation'; +END +$$; +-- Test authorize function +DO $$ +DECLARE + authorized boolean; +BEGIN SELECT - id INTO v_user_role_id + * INTO authorized FROM - keyhippo_rbac.roles - WHERE - name = 'User' - AND group_id = v_user_group_id; - RAISE NOTICE 'Debug: User role ID: %', v_user_role_id; - -- Create permissions - INSERT INTO keyhippo_rbac.permissions (name) - VALUES ('manage_users'), - ('view_reports') - ON CONFLICT (name) - DO NOTHING; - -- Retrieve permission IDs + keyhippo.authorize ('manage_api_keys'); + ASSERT authorized = TRUE, + 'User should be authorized to manage API keys'; SELECT - id INTO v_manage_users_permission_id + * INTO authorized FROM - keyhippo_rbac.permissions - WHERE - name = 'manage_users'; + keyhippo.authorize ('manage_groups'); + ASSERT authorized = TRUE, + 'Admin user should be authorized to manage groups'; +END +$$; +-- Test RBAC functions +DO $$ +DECLARE + t_group_id uuid; + t_role_id uuid; + t_user_id uuid := current_setting('test.user1_id')::uuid; +BEGIN + -- Test create_group SELECT - id INTO v_view_reports_permission_id + * INTO t_group_id FROM - keyhippo_rbac.permissions - WHERE - name = 'view_reports'; - RAISE NOTICE 'Debug: Permissions created/retrieved. manage_users ID: %, view_reports ID: %', v_manage_users_permission_id, v_view_reports_permission_id; - -- Assign permissions to roles - INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id) - VALUES (v_admin_role_id, v_manage_users_permission_id), - (v_admin_role_id, v_view_reports_permission_id), - (v_user_role_id, v_view_reports_permission_id) - ON CONFLICT (role_id, permission_id) - DO NOTHING; - RAISE NOTICE 'Debug: Permissions assigned to roles'; - -- Assign users to roles - INSERT INTO keyhippo_rbac.user_group_roles (user_id, group_id, role_id) - VALUES (v_admin_user_id, v_admin_group_id, v_admin_role_id), - (v_regular_user_id, v_user_group_id, v_user_role_id) - ON CONFLICT (user_id, group_id, role_id) - DO NOTHING; - RAISE NOTICE 'Debug: Users assigned to roles'; - -- Create a test table - CREATE TABLE IF NOT EXISTS public.test_reports ( - id serial PRIMARY KEY, - report_data text - ); - RAISE NOTICE 'Debug: Test table created'; + keyhippo_rbac.create_group ('Test Group', 'A test group'); + ASSERT t_group_id IS NOT NULL, + 'create_group should return a valid group ID'; + -- Test create_role SELECT - oid INTO v_test_table_id + * INTO t_role_id FROM - pg_class - WHERE - relname = 'test_reports'; - RAISE NOTICE 'Debug: Test table ID: %', v_test_table_id; - -- Test admin user permissions - SET LOCAL ROLE authenticated; - PERFORM - set_config('request.jwt.claim.sub', v_admin_user_id::text, TRUE); - RAISE NOTICE 'Debug: About to test admin permissions'; - ASSERT keyhippo.is_authorized (v_test_table_id::regclass, - 'manage_users') = TRUE, - 'Admin should be authorized to manage users'; - RAISE NOTICE 'Debug: Admin manage_users test passed'; - ASSERT keyhippo.is_authorized (v_test_table_id::regclass, - 'view_reports') = TRUE, - 'Admin should be authorized to view reports'; - RAISE NOTICE 'Debug: Admin view_reports test passed'; - -- Test regular user permissions + keyhippo_rbac.create_role ('Test Role', 'A test role', t_group_id, 'user'); + ASSERT t_role_id IS NOT NULL, + 'create_role should return a valid role ID'; + -- Test assign_role_to_user PERFORM - set_config('request.jwt.claim.sub', v_regular_user_id::text, TRUE); - RAISE NOTICE 'Debug: About to test regular user permissions'; - ASSERT keyhippo.is_authorized (v_test_table_id::regclass, - 'manage_users') = FALSE, - 'Regular user should not be authorized to manage users'; - RAISE NOTICE 'Debug: Regular user manage_users test passed'; - ASSERT keyhippo.is_authorized (v_test_table_id::regclass, - 'view_reports') = TRUE, - 'Regular user should be authorized to view reports'; - RAISE NOTICE 'Debug: Regular user view_reports test passed'; - -- Test non-existent permission - ASSERT keyhippo.is_authorized (v_test_table_id::regclass, - 'non_existent_permission') = FALSE, - 'Non-existent permission should return false'; - RAISE NOTICE 'Debug: Non-existent permission test passed'; - -- Test with null user + keyhippo_rbac.assign_role_to_user (t_user_id, t_group_id, t_role_id); + ASSERT EXISTS ( + SELECT + 1 + FROM + keyhippo_rbac.user_group_roles + WHERE + user_id = t_user_id + AND group_id = t_group_id + AND role_id = t_role_id), + 'assign_role_to_user should assign the role to the user'; + -- Test assign_permission_to_role PERFORM - set_config('request.jwt.claim.sub', NULL, TRUE); - RAISE NOTICE 'Debug: About to test null user'; - ASSERT keyhippo.is_authorized (v_test_table_id::regclass, - 'view_reports') = FALSE, - 'Null user should not be authorized'; - RAISE NOTICE 'Debug: Null user test passed'; + keyhippo_rbac.assign_permission_to_role (t_role_id, 'manage_api_keys'); + ASSERT EXISTS ( + SELECT + 1 + FROM + keyhippo_rbac.role_permissions rp + JOIN keyhippo_rbac.permissions p ON rp.permission_id = p.id + WHERE + rp.role_id = t_role_id + AND p.name = 'manage_api_keys'), + 'assign_permission_to_role should assign the permission to the role'; END $$; --- ROLLBACK to ensure no test data persists +-- Clean up ROLLBACK; diff --git a/extension/keyhippo--0.0.40.sql b/extension/keyhippo--0.0.40.sql index f03e487..742b8f6 100644 --- a/extension/keyhippo--0.0.40.sql +++ b/extension/keyhippo--0.0.40.sql @@ -26,74 +26,69 @@ * ██║ ██╗███████╗ ██║ ██║ ██║██║██║ ██║ ╚██████╔╝ * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ */ --- Create necessary schemas +-- Create KeyHippo schema CREATE SCHEMA IF NOT EXISTS keyhippo; +-- Create RBAC schema CREATE SCHEMA IF NOT EXISTS keyhippo_rbac; -CREATE SCHEMA IF NOT EXISTS keyhippo_abac; +-- Create Impersonation schema +CREATE SCHEMA IF NOT EXISTS keyhippo_impersonation; -- Ensure required extensions are installed CREATE EXTENSION IF NOT EXISTS pgcrypto; +-- Create custom types +CREATE TYPE keyhippo.app_permission AS ENUM ( + 'manage_groups', + 'manage_roles', + 'manage_permissions', + 'manage_scopes', + 'manage_user_attributes', + 'manage_api_keys' +); + +CREATE TYPE keyhippo.app_role AS ENUM ( + 'admin', + 'user' +); + -- Create RBAC tables -CREATE TABLE IF NOT EXISTS keyhippo_rbac.groups ( +CREATE TABLE keyhippo_rbac.groups ( id uuid PRIMARY KEY DEFAULT gen_random_uuid (), name text UNIQUE NOT NULL, description text ); -CREATE TABLE IF NOT EXISTS keyhippo_rbac.roles ( +CREATE TABLE keyhippo_rbac.roles ( id uuid PRIMARY KEY DEFAULT gen_random_uuid (), name text NOT NULL, description text, group_id uuid NOT NULL REFERENCES keyhippo_rbac.groups (id) ON DELETE CASCADE, - parent_role_id uuid REFERENCES keyhippo_rbac.roles (id) ON DELETE SET NULL, + role_type keyhippo.app_role NOT NULL DEFAULT 'user', UNIQUE (name, group_id) ); -CREATE TABLE IF NOT EXISTS keyhippo_rbac.permissions ( +CREATE TABLE keyhippo_rbac.permissions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid (), - name text UNIQUE NOT NULL, + name keyhippo.app_permission UNIQUE NOT NULL, description text ); -CREATE TABLE IF NOT EXISTS keyhippo_rbac.role_permissions ( +CREATE TABLE keyhippo_rbac.role_permissions ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, role_id uuid NOT NULL REFERENCES keyhippo_rbac.roles (id) ON DELETE CASCADE, permission_id uuid NOT NULL REFERENCES keyhippo_rbac.permissions (id) ON DELETE CASCADE, - PRIMARY KEY (role_id, permission_id) + UNIQUE (role_id, permission_id) ); -CREATE TABLE IF NOT EXISTS keyhippo_rbac.user_group_roles ( +CREATE TABLE keyhippo_rbac.user_group_roles ( user_id uuid NOT NULL REFERENCES auth.users (id) ON DELETE CASCADE, group_id uuid NOT NULL REFERENCES keyhippo_rbac.groups (id) ON DELETE CASCADE, role_id uuid NOT NULL REFERENCES keyhippo_rbac.roles (id) ON DELETE CASCADE, PRIMARY KEY (user_id, group_id, role_id) ); -CREATE TABLE IF NOT EXISTS keyhippo_rbac.claims_cache ( - user_id uuid PRIMARY KEY REFERENCES auth.users (id) ON DELETE CASCADE, - rbac_claims jsonb DEFAULT '{}' ::jsonb -); - --- Create ABAC tables -CREATE TABLE IF NOT EXISTS keyhippo_abac.user_attributes ( - user_id uuid PRIMARY KEY REFERENCES auth.users (id) ON DELETE CASCADE, - attributes jsonb DEFAULT '{}' ::jsonb -); - -CREATE TABLE IF NOT EXISTS keyhippo_abac.policies ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid (), - name text UNIQUE NOT NULL, - description text, - policy jsonb NOT NULL -); - -CREATE TABLE IF NOT EXISTS keyhippo_abac.group_attributes ( - group_id uuid PRIMARY KEY REFERENCES keyhippo_rbac.groups (id) ON DELETE CASCADE, - attributes jsonb DEFAULT '{}' ::jsonb -); - -- Create KeyHippo tables CREATE TABLE keyhippo.scopes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid (), @@ -101,7 +96,7 @@ CREATE TABLE keyhippo.scopes ( description text ); -CREATE TABLE IF NOT EXISTS keyhippo.scope_permissions ( +CREATE TABLE keyhippo.scope_permissions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid (), scope_id uuid NOT NULL REFERENCES keyhippo.scopes (id), permission_id uuid NOT NULL REFERENCES keyhippo_rbac.permissions (id), @@ -125,491 +120,79 @@ CREATE TABLE keyhippo.api_key_secrets ( key_hash text NOT NULL ); -CREATE OR REPLACE FUNCTION keyhippo.is_authorized (target_resource regclass, required_permission text) - RETURNS boolean - AS $$ -DECLARE - v_user_id uuid; - v_user_id_text text; - v_is_authorized boolean; -BEGIN - -- Get the current user ID from the JWT claim - v_user_id_text := current_setting('request.jwt.claim.sub', TRUE); - -- If no user ID is found or it's an empty string, return false - IF v_user_id_text IS NULL OR v_user_id_text = '' THEN - RETURN FALSE; - END IF; - -- Try to cast the user ID to UUID - BEGIN - v_user_id := v_user_id_text::uuid; - EXCEPTION - WHEN invalid_text_representation THEN - -- If casting fails, return false - RETURN FALSE; - END; - -- Check if the user has the required permission - SELECT - EXISTS ( - SELECT - 1 - FROM - keyhippo_rbac.user_group_roles ugr - JOIN keyhippo_rbac.role_permissions rp ON ugr.role_id = rp.role_id - JOIN keyhippo_rbac.permissions p ON rp.permission_id = p.id - WHERE - ugr.user_id = v_user_id - AND p.name = required_permission) INTO v_is_authorized; - RETURN v_is_authorized; -END; - -$$ -LANGUAGE plpgsql -SECURITY DEFINER; +-- Create Impersonation table +CREATE TABLE IF NOT EXISTS keyhippo_impersonation.impersonation_state ( + impersonated_user_id uuid PRIMARY KEY, + original_role name NOT NULL, + impersonation_time timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); -- Create indexes CREATE INDEX idx_api_key_metadata_user_id ON keyhippo.api_key_metadata (user_id); -CREATE INDEX IF NOT EXISTS idx_user_attributes_gin ON keyhippo_abac.user_attributes USING gin (attributes); - -CREATE INDEX IF NOT EXISTS idx_claims_cache_gin ON keyhippo_rbac.claims_cache USING gin (rbac_claims); - -CREATE INDEX IF NOT EXISTS idx_user_group_roles_user_id ON keyhippo_rbac.user_group_roles (user_id); - -CREATE INDEX IF NOT EXISTS idx_roles_name ON keyhippo_rbac.roles (name); - --- Permissions CRUD --- Create -CREATE OR REPLACE FUNCTION keyhippo_rbac.create_permission (p_name text, p_description text) - RETURNS uuid - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -DECLARE - v_permission_id uuid; -BEGIN - INSERT INTO keyhippo_rbac.permissions (name, description) - VALUES (p_name, p_description) - RETURNING - id INTO v_permission_id; - RETURN v_permission_id; -END; -$$; - --- Read -CREATE OR REPLACE FUNCTION keyhippo_rbac.get_permission (p_permission_id uuid) - RETURNS TABLE ( - id uuid, - name text, - description text) - LANGUAGE sql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ - SELECT - id, - name, - description - FROM - keyhippo_rbac.permissions - WHERE - id = p_permission_id; -$$; - --- Update -CREATE OR REPLACE FUNCTION keyhippo_rbac.update_permission (p_permission_id uuid, p_name text, p_description text) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - UPDATE - keyhippo_rbac.permissions - SET - name = p_name, - description = p_description - WHERE - id = p_permission_id; - RETURN FOUND; -END; -$$; +CREATE INDEX idx_user_group_roles_user_id ON keyhippo_rbac.user_group_roles (user_id); --- Delete -CREATE OR REPLACE FUNCTION keyhippo_rbac.delete_permission (p_permission_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - DELETE FROM keyhippo_rbac.permissions - WHERE id = p_permission_id; - RETURN FOUND; -END; -$$; +CREATE INDEX idx_roles_name ON keyhippo_rbac.roles (name); --- Roles CRUD --- Create -CREATE OR REPLACE FUNCTION keyhippo_rbac.create_role (p_name text, p_description text, p_group_id uuid) - RETURNS uuid +-- Custom access token hook +CREATE OR REPLACE FUNCTION keyhippo.custom_access_token_hook (event JSONB) + RETURNS jsonb LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp + SECURITY DEFINER AS $$ DECLARE - v_role_id uuid; + claims jsonb; + user_role keyhippo.app_role; BEGIN - INSERT INTO keyhippo_rbac.roles (name, description, group_id) - VALUES (p_name, p_description, p_group_id) - RETURNING - id INTO v_role_id; - RETURN v_role_id; -END; -$$; - --- Read -CREATE OR REPLACE FUNCTION keyhippo_rbac.get_role (p_role_id uuid) - RETURNS TABLE ( - id uuid, - name text, - description text, - group_id uuid) - LANGUAGE sql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ + -- Fetch the user role from the user_group_roles table SELECT - id, - name, - description, - group_id + r.role_type INTO user_role FROM - keyhippo_rbac.roles - WHERE - id = p_role_id; -$$; - --- Update -CREATE OR REPLACE FUNCTION keyhippo_rbac.update_role (p_role_id uuid, p_name text, p_description text) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - UPDATE - keyhippo_rbac.roles - SET - name = p_name, - description = p_description + keyhippo_rbac.user_group_roles ugr + JOIN keyhippo_rbac.roles r ON ugr.role_id = r.id WHERE - id = p_role_id; - RETURN FOUND; + ugr.user_id = (event ->> 'user_id')::uuid + LIMIT 1; + claims := event -> 'claims'; + IF user_role IS NOT NULL THEN + -- Set the claim + claims := jsonb_set(claims, '{user_role}', to_jsonb (user_role)); + ELSE + claims := jsonb_set(claims, '{user_role}', 'null'); + END IF; + -- Update the 'claims' object in the original event + event := jsonb_set(event, '{claims}', claims); + -- Return the modified or original event + RETURN event; END; $$; --- Delete -CREATE OR REPLACE FUNCTION keyhippo_rbac.delete_role (p_role_id uuid) +-- Authorization function +CREATE OR REPLACE FUNCTION keyhippo.authorize (requested_permission keyhippo.app_permission) RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - DELETE FROM keyhippo_rbac.roles - WHERE id = p_role_id; - RETURN FOUND; -END; -$$; - --- Groups CRUD --- Create -CREATE OR REPLACE FUNCTION keyhippo_rbac.create_group (p_name text, p_description text) - RETURNS uuid - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp AS $$ DECLARE - v_group_id uuid; -BEGIN - INSERT INTO keyhippo_rbac.groups (name, description) - VALUES (p_name, p_description) - RETURNING - id INTO v_group_id; - RETURN v_group_id; -END; -$$; - --- Read -CREATE OR REPLACE FUNCTION keyhippo_rbac.get_group (p_group_id uuid) - RETURNS TABLE ( - id uuid, - name text, - description text) - LANGUAGE sql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ - SELECT - id, - name, - description - FROM - keyhippo_rbac.groups - WHERE - id = p_group_id; -$$; - --- Update -CREATE OR REPLACE FUNCTION keyhippo_rbac.update_group (p_group_id uuid, p_name text, p_description text) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - UPDATE - keyhippo_rbac.groups - SET - name = p_name, - description = p_description - WHERE - id = p_group_id; - RETURN FOUND; -END; -$$; - --- Delete -CREATE OR REPLACE FUNCTION keyhippo_rbac.delete_group (p_group_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - DELETE FROM keyhippo_rbac.groups - WHERE id = p_group_id; - RETURN FOUND; -END; -$$; - --- User Group Roles CRUD --- Read -CREATE OR REPLACE FUNCTION keyhippo_rbac.get_user_group_roles (p_user_id uuid) - RETURNS TABLE ( - user_id uuid, - group_id uuid, - role_id uuid) - LANGUAGE sql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ - SELECT - user_id, - group_id, - role_id - FROM - keyhippo_rbac.user_group_roles - WHERE - user_id = p_user_id; -$$; - --- Delete -CREATE OR REPLACE FUNCTION keyhippo_rbac.remove_user_group_role (p_user_id uuid, p_group_id uuid, p_role_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ + bind_permissions int; + user_role keyhippo.app_role; BEGIN - DELETE FROM keyhippo_rbac.user_group_roles - WHERE user_id = p_user_id - AND group_id = p_group_id - AND role_id = p_role_id; - RETURN FOUND; -END; -$$; - --- ABAC Policies CRUD --- Read -CREATE OR REPLACE FUNCTION keyhippo_abac.get_policy (p_policy_id uuid) - RETURNS TABLE ( - id uuid, - name text, - description text, - POLICY jsonb) - LANGUAGE sql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ + -- Fetch user role from the JWT SELECT - id, - name, - description, - POLICY - FROM - keyhippo_abac.policies - WHERE - id = p_policy_id; -$$; - --- Update -CREATE OR REPLACE FUNCTION keyhippo_abac.update_policy (p_policy_id uuid, p_name text, p_description text, p_policy jsonb) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - UPDATE - keyhippo_abac.policies - SET - name = p_name, - description = p_description, - POLICY = p_policy - WHERE - id = p_policy_id; - RETURN FOUND; -END; -$$; - --- Delete -CREATE OR REPLACE FUNCTION keyhippo_abac.delete_policy (p_policy_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - DELETE FROM keyhippo_abac.policies - WHERE id = p_policy_id; - RETURN FOUND; -END; -$$; - --- Scopes CRUD --- Create -CREATE OR REPLACE FUNCTION keyhippo.create_scope (p_name text, p_description text) - RETURNS uuid - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -DECLARE - v_scope_id uuid; -BEGIN - INSERT INTO keyhippo.scopes (name, description) - VALUES (p_name, p_description) - RETURNING - id INTO v_scope_id; - RETURN v_scope_id; -END; -$$; - --- Read -CREATE OR REPLACE FUNCTION keyhippo.get_scope (p_scope_id uuid) - RETURNS TABLE ( - id uuid, - name text, - description text) - LANGUAGE sql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ + (current_setting('request.jwt.claims', TRUE)::jsonb ->> 'user_role')::keyhippo.app_role INTO user_role; SELECT - id, - name, - description + COUNT(*) INTO bind_permissions FROM - keyhippo.scopes - WHERE - id = p_scope_id; -$$; - --- Update -CREATE OR REPLACE FUNCTION keyhippo.update_scope (p_scope_id uuid, p_name text, p_description text) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - UPDATE - keyhippo.scopes - SET - name = p_name, - description = p_description + keyhippo_rbac.role_permissions rp + JOIN keyhippo_rbac.roles r ON rp.role_id = r.id + JOIN keyhippo_rbac.permissions p ON rp.permission_id = p.id WHERE - id = p_scope_id; - RETURN FOUND; -END; -$$; - --- Delete -CREATE OR REPLACE FUNCTION keyhippo.delete_scope (p_scope_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - DELETE FROM keyhippo.scopes - WHERE id = p_scope_id; - RETURN FOUND; -END; -$$; - --- Add permission to scope -CREATE OR REPLACE FUNCTION keyhippo.add_permission_to_scope (p_scope_id uuid, p_permission_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - INSERT INTO keyhippo.scope_permissions (scope_id, permission_id) - VALUES (p_scope_id, p_permission_id) - ON CONFLICT (scope_id, permission_id) - DO NOTHING; - RETURN FOUND; -END; -$$; - --- Remove permission from scope -CREATE OR REPLACE FUNCTION keyhippo.remove_permission_from_scope (p_scope_id uuid, p_permission_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ -BEGIN - DELETE FROM keyhippo.scope_permissions - WHERE scope_id = p_scope_id - AND permission_id = p_permission_id; - RETURN FOUND; + p.name = requested_permission + AND r.role_type = user_role; + RETURN bind_permissions > 0; END; -$$; - --- Get permissions for a scope -CREATE OR REPLACE FUNCTION keyhippo.get_scope_permissions (p_scope_id uuid) - RETURNS TABLE ( - permission_id uuid, - permission_name text) - LANGUAGE sql - SECURITY INVOKER - SET search_path = pg_temp - AS $$ - SELECT - p.id, - p.name - FROM - keyhippo.scope_permissions sp - JOIN keyhippo_rbac.permissions p ON sp.permission_id = p.id - WHERE - sp.scope_id = p_scope_id; -$$; +$$ +LANGUAGE plpgsql +STABLE +SECURITY DEFINER SET search_path = ''; -- Function to create an API key CREATE OR REPLACE FUNCTION keyhippo.create_api_key (key_description text, scope_name text DEFAULT NULL) @@ -618,7 +201,6 @@ CREATE OR REPLACE FUNCTION keyhippo.create_api_key (key_description text, scope_ api_key_id uuid) LANGUAGE plpgsql SECURITY DEFINER - SET search_path = pg_temp AS $$ DECLARE random_bytes bytea; @@ -678,7 +260,6 @@ CREATE OR REPLACE FUNCTION keyhippo.verify_api_key (api_key text) permissions text[]) LANGUAGE plpgsql SECURITY DEFINER - SET search_path = pg_temp AS $$ DECLARE metadata_id uuid; @@ -724,39 +305,32 @@ BEGIN key_metadata_id = metadata_id; computed_hash := encode(extensions.digest(key_part, 'sha512'), 'hex'); IF computed_hash = stored_key_hash THEN - BEGIN - -- Update last_used_at if necessary - UPDATE - keyhippo.api_key_metadata - SET - last_used_at = NOW() - WHERE - id = metadata_id - AND (last_used_at IS NULL - OR last_used_at < NOW() - INTERVAL '1 minute'); - EXCEPTION - WHEN read_only_sql_transaction THEN - -- Handle read-only transaction error - RAISE NOTICE 'Could not update last_used_at in read-only transaction'; - END; - -- Return user_id, scope_id, and permissions - RETURN QUERY - SELECT + -- Update last_used_at if necessary + UPDATE + keyhippo.api_key_metadata + SET + last_used_at = NOW() + WHERE + id = metadata_id + AND (last_used_at IS NULL + OR last_used_at < NOW() - INTERVAL '1 minute'); + -- Return user_id, scope_id, and permissions + RETURN QUERY + SELECT + v_user_id, + v_scope_id, + ARRAY_AGG(DISTINCT p.name::text) + FROM + keyhippo.api_key_metadata akm + LEFT JOIN keyhippo.scope_permissions sp ON akm.scope_id = sp.scope_id + LEFT JOIN keyhippo_rbac.permissions p ON sp.permission_id = p.id + WHERE + akm.id = metadata_id + GROUP BY v_user_id, - v_scope_id, - ARRAY_AGG(DISTINCT p.name)::text[] - FROM - keyhippo.api_key_metadata akm - LEFT JOIN keyhippo.get_scope_permissions (akm.scope_id) sp ON TRUE - LEFT JOIN keyhippo_rbac.get_permission (sp.permission_id) p ON TRUE -WHERE - akm.id = metadata_id -GROUP BY - v_user_id, - v_scope_id; + v_scope_id; END IF; END; - $$; -- Function to get user_id and scope from API key or JWT @@ -767,7 +341,6 @@ CREATE OR REPLACE FUNCTION keyhippo.current_user_context () permissions text[]) LANGUAGE plpgsql SECURITY INVOKER - SET search_path = pg_temp AS $$ DECLARE api_key text; @@ -799,13 +372,13 @@ BEGIN RETURN; END IF; SELECT - ARRAY_AGG(DISTINCT p.name)::text[] INTO v_permissions + ARRAY_AGG(DISTINCT p.name::text) INTO v_permissions FROM keyhippo_rbac.user_group_roles ugr JOIN keyhippo_rbac.role_permissions rp ON ugr.role_id = rp.role_id - JOIN keyhippo_rbac.get_permission (rp.permission_id) p ON TRUE -WHERE - ugr.user_id = v_user_id; + JOIN keyhippo_rbac.permissions p ON rp.permission_id = p.id + WHERE + ugr.user_id = v_user_id; RETURN QUERY SELECT v_user_id, @@ -819,7 +392,6 @@ CREATE OR REPLACE FUNCTION keyhippo.revoke_api_key (api_key_id uuid) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER - SET search_path = pg_temp AS $$ DECLARE success boolean; @@ -865,7 +437,6 @@ CREATE OR REPLACE FUNCTION keyhippo.rotate_api_key (old_api_key_id uuid) new_api_key_id uuid) LANGUAGE plpgsql SECURITY DEFINER - SET search_path = pg_temp AS $$ DECLARE c_user_id uuid; @@ -909,248 +480,66 @@ BEGIN keyhippo.create_api_key (key_description, ( SELECT name - FROM keyhippo.get_scope (key_scope_id))); + FROM keyhippo.scopes + WHERE + id = key_scope_id)); END; $$; --- Pre-request function to check API key and set user context -CREATE OR REPLACE FUNCTION keyhippo.check_request () - RETURNS void +-- RBAC management functions +CREATE OR REPLACE FUNCTION keyhippo_rbac.create_group (p_name text, p_description text) + RETURNS uuid LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp + SECURITY INVOKER AS $$ DECLARE - req_api_key text := current_setting('request.header.x-api-key', TRUE); - verified_user_id uuid; - verified_scope_id uuid; - verified_permissions text[]; + v_group_id uuid; BEGIN - IF req_api_key IS NULL THEN - -- No API key provided, continue with normal auth - RETURN; - END IF; - SELECT - user_id, - scope_id, - permissions INTO verified_user_id, - verified_scope_id, - verified_permissions - FROM - keyhippo.verify_api_key (req_api_key); - IF verified_user_id IS NULL THEN - -- No valid API key found, raise an error - RAISE EXCEPTION 'Invalid API key provided in x-api-key header.'; - ELSE - -- Set the user context for RLS policies - PERFORM - set_config('request.jwt.claim.sub', verified_user_id::text, TRUE); - PERFORM - set_config('request.jwt.claim.scope', verified_scope_id::text, TRUE); - PERFORM - set_config('request.jwt.claim.permissions', array_to_json(verified_permissions)::text, TRUE); - END IF; + INSERT INTO keyhippo_rbac.groups (name, description) + VALUES (p_name, p_description) + RETURNING + id INTO v_group_id; + RETURN v_group_id; END; $$; --- RBAC Functions -CREATE OR REPLACE FUNCTION keyhippo_rbac.assign_role_to_user (p_user_id uuid, p_group_id uuid, p_role_name text) - RETURNS void +CREATE OR REPLACE FUNCTION keyhippo_rbac.create_role (p_name text, p_description text, p_group_id uuid, p_role_type keyhippo.app_role) + RETURNS uuid LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp + SECURITY INVOKER AS $$ DECLARE v_role_id uuid; - v_current_user_id uuid; -BEGIN - -- Get the current user context - SELECT - user_id INTO v_current_user_id - FROM - keyhippo.current_user_context (); - -- Check if the current user has 'manage_roles' permission in the specified group - IF NOT EXISTS ( - SELECT - 1 - FROM - keyhippo_rbac.user_group_roles ugr - JOIN keyhippo_rbac.role_permissions rp ON ugr.role_id = rp.role_id - JOIN keyhippo_rbac.permissions p ON rp.permission_id = p.id - WHERE - ugr.user_id = v_current_user_id - AND ugr.group_id = p_group_id - AND p.name = 'manage_roles') THEN - RAISE EXCEPTION 'Unauthorized to assign roles in this group'; -END IF; - -- Get the role ID - SELECT - id INTO v_role_id - FROM - keyhippo_rbac.roles - WHERE - name = p_role_name - AND group_id = p_group_id; - IF v_role_id IS NULL THEN - RAISE EXCEPTION 'Role % not found in the specified group', p_role_name; - END IF; - -- Insert or update the user_group_roles entry - INSERT INTO keyhippo_rbac.user_group_roles (user_id, group_id, role_id) - VALUES (p_user_id, p_group_id, v_role_id) - ON CONFLICT (user_id, group_id, role_id) - DO UPDATE SET - role_id = EXCLUDED.role_id; - -- Update the claims cache - PERFORM - keyhippo_rbac.update_user_claims_cache (p_user_id); -END; -$$; - -CREATE OR REPLACE FUNCTION keyhippo_rbac.set_parent_role (p_child_role_id uuid, p_new_parent_role_id uuid) - RETURNS TABLE ( - updated_parent_role_id uuid) - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ -DECLARE - v_parent_role_id uuid; BEGIN - -- Prevent circular hierarchy - IF p_new_parent_role_id IS NOT NULL THEN - IF EXISTS ( WITH RECURSIVE role_hierarchy AS ( - SELECT - id, - parent_role_id - FROM - keyhippo_rbac.roles - WHERE - id = p_new_parent_role_id - UNION - SELECT - r.id, - r.parent_role_id - FROM - keyhippo_rbac.roles r - INNER JOIN role_hierarchy rh ON r.id = rh.parent_role_id -) - SELECT - 1 - FROM - role_hierarchy - WHERE - id = p_child_role_id) THEN - RAISE EXCEPTION 'Circular role hierarchy detected'; - END IF; -END IF; - -- Set the parent role - UPDATE - keyhippo_rbac.roles - SET - parent_role_id = p_new_parent_role_id - WHERE - id = p_child_role_id + INSERT INTO keyhippo_rbac.roles (name, description, group_id, role_type) + VALUES (p_name, p_description, p_group_id, p_role_type) RETURNING - parent_role_id INTO v_parent_role_id; - -- Return the updated parent_role_id - RETURN QUERY - SELECT - v_parent_role_id; -END; -$$; - -CREATE OR REPLACE FUNCTION keyhippo_rbac.update_user_claims_cache (p_user_id uuid) - RETURNS void - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ -DECLARE - v_claims jsonb; -BEGIN - IF p_user_id IS NULL THEN - RAISE EXCEPTION 'update_user_claims_cache called with NULL user_id'; - END IF; - -- Delete existing claims for the user - DELETE FROM keyhippo_rbac.claims_cache - WHERE user_id = p_user_id; - -- Gather claims data - WITH group_roles AS ( - SELECT - g.id AS group_id, - array_agg(DISTINCT r.name) AS role_names - FROM - keyhippo_rbac.user_group_roles ugr - JOIN keyhippo_rbac.roles r ON ugr.role_id = r.id - JOIN keyhippo_rbac.groups g ON ugr.group_id = g.id - WHERE - ugr.user_id = p_user_id - GROUP BY - g.id -) - SELECT - jsonb_object_agg(group_id::text, role_names) INTO v_claims - FROM - group_roles; - -- Insert new claims - IF v_claims IS NOT NULL THEN - INSERT INTO keyhippo_rbac.claims_cache (user_id, rbac_claims) - VALUES (p_user_id, v_claims); - ELSE - -- If no claims were gathered, ensure an empty claims cache entry exists - INSERT INTO keyhippo_rbac.claims_cache (user_id, rbac_claims) - VALUES (p_user_id, '{}') - ON CONFLICT (user_id) - DO NOTHING; - END IF; + id INTO v_role_id; + RETURN v_role_id; END; $$; -CREATE OR REPLACE FUNCTION keyhippo_rbac.user_has_permission (permission_name text) - RETURNS boolean +CREATE OR REPLACE FUNCTION keyhippo_rbac.assign_role_to_user (p_user_id uuid, p_group_id uuid, p_role_id uuid) + RETURNS VOID LANGUAGE plpgsql - SECURITY DEFINER + SECURITY INVOKER AS $$ -DECLARE - current_user_id uuid; BEGIN - SELECT - user_id INTO current_user_id - FROM - keyhippo.current_user_context (); - RETURN EXISTS ( - SELECT - 1 - FROM - keyhippo_rbac.user_group_roles ugr - JOIN keyhippo_rbac.role_permissions rp ON ugr.role_id = rp.role_id - JOIN keyhippo_rbac.get_permission (rp.permission_id) p ON TRUE - WHERE - ugr.user_id = current_user_id - AND p.name = permission_name); + INSERT INTO keyhippo_rbac.user_group_roles (user_id, group_id, role_id) + VALUES (p_user_id, p_group_id, p_role_id) + ON CONFLICT (user_id, group_id, role_id) + DO NOTHING; END; $$; -CREATE OR REPLACE FUNCTION keyhippo_rbac.assign_permission_to_role (p_role_id uuid, p_permission_name text) - RETURNS void +CREATE OR REPLACE FUNCTION keyhippo_rbac.assign_permission_to_role (p_role_id uuid, p_permission_name keyhippo.app_permission) + RETURNS VOID LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp + SECURITY INVOKER AS $$ DECLARE v_permission_id uuid; - v_current_user_id uuid; BEGIN - -- Get the current user context - SELECT - user_id INTO v_current_user_id - FROM - keyhippo.current_user_context (); - -- Check if the current user has 'manage_roles' permission - IF NOT keyhippo_rbac.user_has_permission ('manage_roles') THEN - RAISE EXCEPTION 'Unauthorized to assign permissions to roles'; - END IF; - -- Get the permission ID SELECT id INTO v_permission_id FROM @@ -1160,275 +549,140 @@ BEGIN IF v_permission_id IS NULL THEN RAISE EXCEPTION 'Permission % not found', p_permission_name; END IF; - -- Insert the role-permission relationship INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id) VALUES (p_role_id, v_permission_id) ON CONFLICT (role_id, permission_id) DO NOTHING; - -- Update the claims cache for all users with this role - -- This is a simplified approach; you might want to optimize this for large user bases - PERFORM - keyhippo_rbac.update_user_claims_cache (user_id) - FROM - keyhippo_rbac.user_group_roles - WHERE - role_id = p_role_id; END; $$; -CREATE OR REPLACE FUNCTION keyhippo_rbac.add_user_to_group (p_user_id uuid, p_group_id uuid, p_role_name text) - RETURNS void - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ +-- Impersonation functions +CREATE OR REPLACE PROCEDURE keyhippo_impersonation.login_as_user (user_id uuid) +LANGUAGE plpgsql +SECURITY INVOKER +AS $$ DECLARE - v_role_id uuid; + auth_user auth.users%ROWTYPE; + CURRENT_ROLE NAME; BEGIN - -- Retrieve the role ID based on the role name and group ID + -- Ensure only postgres user can call this procedure + IF CURRENT_USER != 'postgres' THEN + RAISE EXCEPTION 'Unauthorized'; + END IF; + -- Get current role before impersonation + SELECT + CURRENT_ROLE INTO CURRENT_ROLE; + -- Fetch the user based on UUID SELECT - id INTO v_role_id + * INTO auth_user FROM - keyhippo_rbac.roles + auth.users WHERE - name = p_role_name - AND group_id = p_group_id; - IF v_role_id IS NULL THEN - RAISE EXCEPTION 'Role % not found in Group %', p_role_name, p_group_id; - END IF; - -- Insert or update the user_group_roles table - INSERT INTO keyhippo_rbac.user_group_roles (user_id, group_id, role_id) - VALUES (p_user_id, p_group_id, v_role_id) - ON CONFLICT (user_id, group_id, role_id) - DO NOTHING; - -- Update the claims cache - PERFORM - keyhippo_rbac.update_user_claims_cache (p_user_id); -END; -$$; - -CREATE OR REPLACE FUNCTION keyhippo_rbac.remove_user_from_group (p_user_id uuid, p_group_id uuid) - RETURNS void - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ -BEGIN - -- Remove the user from the user_group_roles table for the specified group - DELETE FROM keyhippo_rbac.user_group_roles - WHERE user_id = p_user_id - AND group_id = p_group_id; - -- Check if any rows were affected + id = user_id; IF NOT FOUND THEN - RAISE EXCEPTION 'User % is not a member of Group %', p_user_id, p_group_id; + RAISE EXCEPTION 'User with ID % does not exist', user_id; END IF; - -- Update the claims cache + -- Set JWT claims using parameterized queries PERFORM - keyhippo_rbac.update_user_claims_cache (p_user_id); -END; -$$; - --- ABAC Functions -CREATE OR REPLACE FUNCTION keyhippo_abac.set_user_attribute (p_user_id uuid, p_attribute text, p_value jsonb) - RETURNS void - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ -DECLARE - v_current_user_id uuid; -BEGIN - -- Get the current user context - SELECT - user_id INTO v_current_user_id - FROM - keyhippo.current_user_context (); - -- Check if the current user has 'manage_user_attributes' permission - IF NOT keyhippo_rbac.user_has_permission ('manage_user_attributes') THEN - RAISE EXCEPTION 'Unauthorized to set user attributes'; + set_config('request.jwt.claim.sub', auth_user.id::text, TRUE); + PERFORM + set_config('request.jwt.claim.role', COALESCE(auth_user.role, 'authenticated'), TRUE); + IF auth_user.email IS NOT NULL THEN + PERFORM + set_config('request.jwt.claim.email', auth_user.email, TRUE); END IF; - INSERT INTO keyhippo_abac.user_attributes (user_id, attributes) - VALUES (p_user_id, jsonb_build_object(p_attribute, p_value)) - ON CONFLICT (user_id) + IF auth_user.raw_app_meta_data IS NOT NULL THEN + PERFORM + set_config('request.jwt.claims', JSON_STRIP_NULLS (JSON_BUILD_OBJECT('app_metadata', auth_user.raw_app_meta_data))::text, TRUE); + END IF; + -- Track impersonation state + INSERT INTO keyhippo_impersonation.impersonation_state (impersonated_user_id, original_role) + VALUES (user_id, CURRENT_ROLE) + ON CONFLICT (impersonated_user_id) DO UPDATE SET - attributes = keyhippo_abac.user_attributes.attributes || jsonb_build_object(p_attribute, p_value); + original_role = EXCLUDED.original_role, impersonation_time = CURRENT_TIMESTAMP; + -- Set role + EXECUTE FORMAT('SET ROLE %I', COALESCE(auth_user.role, 'authenticated')); + -- Set session timeout + PERFORM + set_config('session.impersonation_expires', (NOW() + INTERVAL '1 hour')::text, TRUE); END; $$; -CREATE OR REPLACE FUNCTION keyhippo_abac.check_abac_policy (p_user_id uuid, p_policy jsonb) - RETURNS boolean - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ +CREATE OR REPLACE PROCEDURE keyhippo_impersonation.login_as_anon () +LANGUAGE plpgsql +SECURITY INVOKER +AS $$ DECLARE - v_user_attributes jsonb; - v_policy_type text; - v_policy_attribute text; - v_policy_value jsonb; - v_user_attribute_value jsonb; + CURRENT_ROLE NAME; + ANON_USER_ID uuid := '00000000-0000-0000-0000-000000000000'::uuid; BEGIN - -- Get user attributes - SELECT - attributes INTO v_user_attributes - FROM - keyhippo_abac.user_attributes - WHERE - user_id = p_user_id; - -- If user attributes are not found, return FALSE immediately - IF v_user_attributes IS NULL THEN - RETURN FALSE; - END IF; - -- Extract policy details - v_policy_type := p_policy ->> 'type'; - -- Handle 'and' and 'or' policy types - IF v_policy_type = 'and' OR v_policy_type = 'or' THEN - DECLARE v_result boolean; - i integer; - BEGIN - v_result := (v_policy_type = 'and'); - FOR i IN 0..jsonb_array_length(p_policy -> 'conditions') - 1 LOOP - IF v_policy_type = 'and' THEN - v_result := v_result - AND keyhippo_abac.check_abac_policy (p_user_id, p_policy -> 'conditions' -> i); - EXIT - WHEN NOT v_result; - ELSIF v_policy_type = 'or' THEN - v_result := v_result - OR keyhippo_abac.check_abac_policy (p_user_id, p_policy -> 'conditions' -> i); - EXIT - WHEN v_result; - END IF; - END LOOP; - RETURN v_result; - END; - END IF; - -- Handle attribute-based policy types - v_policy_attribute := p_policy ->> 'attribute'; - v_policy_value := p_policy -> 'value'; - -- Get the user's attribute value - v_user_attribute_value := v_user_attributes -> v_policy_attribute; - -- Check policy - IF v_policy_type = 'attribute_equals' THEN - IF v_user_attribute_value IS NULL THEN - RETURN FALSE; - END IF; - RETURN v_user_attribute_value = v_policy_value; - ELSIF v_policy_type = 'attribute_contains' THEN - IF v_user_attribute_value IS NULL THEN - RETURN FALSE; - END IF; - RETURN v_user_attribute_value @> v_policy_value; - ELSIF v_policy_type = 'attribute_contained_by' THEN - IF v_user_attribute_value IS NULL THEN - RETURN FALSE; - END IF; - RETURN v_user_attribute_value <@ v_policy_value; - ELSE - RAISE EXCEPTION 'Unsupported policy type: %', v_policy_type; + -- Ensure only postgres user can call this procedure + IF CURRENT_USER != 'postgres' THEN + RAISE EXCEPTION 'Unauthorized'; END IF; + -- Get current role before impersonation + SELECT + CURRENT_ROLE INTO CURRENT_ROLE; + -- Set JWT claims for anonymous login + PERFORM + set_config('request.jwt.claim.sub', 'anon', TRUE); + PERFORM + set_config('request.jwt.claim.role', 'anon', TRUE); + PERFORM + set_config('request.jwt.claim.email', '', TRUE); + PERFORM + set_config('request.jwt.claims', json_build_object('sub', 'anon', 'role', 'anon')::text, TRUE); + -- Track impersonation state + DELETE FROM keyhippo_impersonation.impersonation_state + WHERE impersonated_user_id = ANON_USER_ID; + INSERT INTO keyhippo_impersonation.impersonation_state (impersonated_user_id, original_role) + VALUES (ANON_USER_ID, CURRENT_ROLE); + -- Set role to anon + SET ROLE anon; + -- Set session timeout + PERFORM + set_config('session.impersonation_expires', (NOW() + INTERVAL '1 hour')::text, TRUE); + RAISE NOTICE 'Anonymous login successful. Impersonated user ID: %, Original role: %, Session expires: %', ANON_USER_ID, CURRENT_ROLE, current_setting('session.impersonation_expires', TRUE); END; $$; -CREATE OR REPLACE FUNCTION keyhippo_abac.evaluate_policies (p_user_id uuid) - RETURNS boolean - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ +CREATE OR REPLACE PROCEDURE keyhippo_impersonation.logout () +LANGUAGE plpgsql +AS $$ DECLARE - policy_record RECORD; - v_user_attributes jsonb; + v_original_role text; BEGIN - -- Get user attributes + IF current_setting('session.impersonation_expires', TRUE) IS NULL THEN + RAISE EXCEPTION 'Not in an impersonation session'; + END IF; SELECT - attributes INTO v_user_attributes + original_role::text INTO v_original_role FROM - keyhippo_abac.user_attributes + keyhippo_impersonation.impersonation_state WHERE - user_id = p_user_id; - -- If user attributes are not found, return FALSE immediately - IF v_user_attributes IS NULL THEN - RETURN FALSE; + impersonated_user_id = '00000000-0000-0000-0000-000000000000'::uuid; + IF v_original_role IS NULL THEN + RAISE EXCEPTION 'Current user is not being impersonated'; END IF; - -- Loop through all policies and evaluate them - FOR policy_record IN - SELECT - * - FROM - keyhippo_abac.policies LOOP - IF NOT keyhippo_abac.check_abac_policy (p_user_id, policy_record.policy) THEN - RETURN FALSE; - END IF; - END LOOP; - RETURN TRUE; -END; -$$; - -CREATE OR REPLACE FUNCTION keyhippo_abac.create_policy (p_name text, p_description text, p_policy jsonb) - RETURNS uuid - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path = pg_temp - AS $$ -DECLARE - v_current_user_id uuid; - v_policy_id uuid; - v_policy_type text; - v_attribute text; - v_value jsonb; -BEGIN - -- Get the current user context - SELECT - user_id INTO v_current_user_id - FROM - keyhippo.current_user_context (); - -- TODO: Create a user in the pgtap tests with the manage_policies permission - -- and re-enable the following check: - -- Check if the current user has 'manage_policies' permission - --IF NOT keyhippo_rbac.user_has_permission ('manage_policies') THEN - ---- RAISE EXCEPTION 'Unauthorized to create policies'; - --END IF; - -- Validate policy format - v_policy_type := p_policy ->> 'type'; - IF v_policy_type IS NULL THEN - RAISE EXCEPTION 'Invalid policy format: type is missing' - USING ERRCODE = 'P0001'; - END IF; - CASE v_policy_type - WHEN 'attribute_equals', - 'attribute_contains', - 'attribute_contained_by' THEN - v_attribute := p_policy ->> 'attribute'; v_value := p_policy -> 'value'; IF v_attribute IS NULL OR v_value IS NULL THEN - RAISE EXCEPTION 'Invalid policy format: attribute or value is missing' - USING ERRCODE = 'P0001'; - END IF; - IF jsonb_typeof(v_value) - NOT IN ('string', 'boolean', 'null') THEN - RAISE EXCEPTION 'Invalid policy format: value must be a string, boolean, or null' - USING ERRCODE = 'P0001'; - END IF; - WHEN 'and', - 'or' THEN - IF p_policy -> 'conditions' IS NULL OR jsonb_typeof(p_policy -> 'conditions') != 'array' THEN - RAISE EXCEPTION 'Invalid policy format: conditions must be an array' - USING ERRCODE = 'P0001'; - END IF; - ELSE - RAISE EXCEPTION 'Invalid policy format: unsupported type %', v_policy_type - USING ERRCODE = 'P0001'; - END CASE; - -- Insert the new policy - INSERT INTO keyhippo_abac.policies (name, description, POLICY) - VALUES (p_name, p_description, p_policy) - RETURNING - id INTO v_policy_id; - RETURN v_policy_id; + PERFORM + set_config('request.jwt.claim.sub', '', TRUE); + PERFORM + set_config('request.jwt.claim.role', '', TRUE); + PERFORM + set_config('request.jwt.claim.email', '', TRUE); + PERFORM + set_config('request.jwt.claims', '', TRUE); + EXECUTE FORMAT('SET ROLE %I', v_original_role); + DELETE FROM keyhippo_impersonation.impersonation_state + WHERE impersonated_user_id = '00000000-0000-0000-0000-000000000000'::uuid; + PERFORM + set_config('session.impersonation_expires', '', TRUE); + RAISE NOTICE 'Logout successful. Original role: %', v_original_role; END; $$; -- RLS Policies --- Enable RLS on all tables ALTER TABLE keyhippo_rbac.groups ENABLE ROW LEVEL SECURITY; ALTER TABLE keyhippo_rbac.roles ENABLE ROW LEVEL SECURITY; @@ -1439,12 +693,6 @@ ALTER TABLE keyhippo_rbac.role_permissions ENABLE ROW LEVEL SECURITY; ALTER TABLE keyhippo_rbac.user_group_roles ENABLE ROW LEVEL SECURITY; -ALTER TABLE keyhippo_rbac.claims_cache ENABLE ROW LEVEL SECURITY; - -ALTER TABLE keyhippo_abac.user_attributes ENABLE ROW LEVEL SECURITY; - -ALTER TABLE keyhippo_abac.policies ENABLE ROW LEVEL SECURITY; - ALTER TABLE keyhippo.scopes ENABLE ROW LEVEL SECURITY; ALTER TABLE keyhippo.scope_permissions ENABLE ROW LEVEL SECURITY; @@ -1453,101 +701,62 @@ ALTER TABLE keyhippo.api_key_metadata ENABLE ROW LEVEL SECURITY; ALTER TABLE keyhippo.api_key_secrets ENABLE ROW LEVEL SECURITY; +ALTER TABLE keyhippo_impersonation.impersonation_state ENABLE ROW LEVEL SECURITY; + -- Create RLS policies CREATE POLICY groups_access_policy ON keyhippo_rbac.groups FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_groups')); - -CREATE POLICY groups_auth_admin_policy ON keyhippo_rbac.groups - FOR ALL TO supabase_auth_admin - USING (TRUE) - WITH CHECK (TRUE); - -CREATE POLICY permissions_access_policy ON keyhippo_rbac.permissions - FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_permissions')); + USING (keyhippo.authorize ('manage_groups')) + WITH CHECK (keyhippo.authorize ('manage_groups')); CREATE POLICY roles_access_policy ON keyhippo_rbac.roles FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_roles')); + USING (keyhippo.authorize ('manage_roles')) + WITH CHECK (keyhippo.authorize ('manage_roles')); -CREATE POLICY roles_auth_admin_policy ON keyhippo_rbac.roles - FOR ALL TO supabase_auth_admin - USING (TRUE) - WITH CHECK (TRUE); +CREATE POLICY permissions_access_policy ON keyhippo_rbac.permissions + FOR ALL TO authenticated + USING (keyhippo.authorize ('manage_permissions')) + WITH CHECK (keyhippo.authorize ('manage_permissions')); CREATE POLICY role_permissions_access_policy ON keyhippo_rbac.role_permissions FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_roles')); + USING (keyhippo.authorize ('manage_roles')) + WITH CHECK (keyhippo.authorize ('manage_roles')); CREATE POLICY user_group_roles_access_policy ON keyhippo_rbac.user_group_roles FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_roles')); - -CREATE POLICY claims_cache_access_policy ON keyhippo_rbac.claims_cache - FOR SELECT TO authenticated - USING (( - SELECT - user_id - FROM - keyhippo.current_user_context ()) = keyhippo_rbac.claims_cache.user_id); - -CREATE POLICY claims_cache_auth_admin_policy ON keyhippo_rbac.claims_cache - FOR SELECT TO supabase_auth_admin - USING (keyhippo_rbac.claims_cache.user_id = current_setting('request.jwt.claim.sub', TRUE)::uuid); - -CREATE POLICY user_attributes_access_policy ON keyhippo_abac.user_attributes - FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_user_attributes')); - -CREATE POLICY policies_access_policy ON keyhippo_abac.policies - FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_policies')); - -CREATE POLICY policies_auth_admin_policy ON keyhippo_abac.policies - FOR ALL TO supabase_auth_admin - USING (TRUE) - WITH CHECK (TRUE); + USING (keyhippo.authorize ('manage_roles')) + WITH CHECK (keyhippo.authorize ('manage_roles')); CREATE POLICY scopes_access_policy ON keyhippo.scopes FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_scopes')); + USING (keyhippo.authorize ('manage_scopes')) + WITH CHECK (keyhippo.authorize ('manage_scopes')); CREATE POLICY scope_permissions_access_policy ON keyhippo.scope_permissions FOR ALL TO authenticated - USING (TRUE) - WITH CHECK (keyhippo_rbac.user_has_permission ('manage_scopes')); + USING (keyhippo.authorize ('manage_scopes')) + WITH CHECK (keyhippo.authorize ('manage_scopes')); CREATE POLICY api_key_metadata_access_policy ON keyhippo.api_key_metadata FOR ALL TO authenticated - USING (user_id = ( - SELECT - user_id - FROM - keyhippo.current_user_context ())); - -CREATE POLICY api_key_metadata_auth_admin_policy ON keyhippo.api_key_metadata - FOR ALL TO supabase_auth_admin - USING (TRUE); + USING (user_id = auth.uid () + OR keyhippo.authorize ('manage_api_keys')); CREATE POLICY api_key_secrets_no_access_policy ON keyhippo.api_key_secrets FOR ALL TO authenticated USING (FALSE); --- Permissions and Grants -GRANT USAGE ON SCHEMA keyhippo TO authenticated, service_role; +CREATE POLICY impersonation_state_access ON keyhippo_impersonation.impersonation_state + USING (CURRENT_USER = 'postgres' + OR (CURRENT_USER = 'anon' AND impersonated_user_id = '00000000-0000-0000-0000-000000000000'::uuid) + OR impersonated_user_id::text = CURRENT_USER); -GRANT USAGE ON SCHEMA keyhippo_rbac TO authenticated, service_role; +-- Grants and Permissions +GRANT USAGE ON SCHEMA keyhippo TO authenticated, anon; -GRANT USAGE ON SCHEMA keyhippo_abac TO authenticated, service_role; +GRANT USAGE ON SCHEMA keyhippo_rbac TO authenticated, anon; -- Grant EXECUTE on functions GRANT EXECUTE ON FUNCTION keyhippo.create_api_key (text, text) TO authenticated; @@ -1556,31 +765,25 @@ GRANT EXECUTE ON FUNCTION keyhippo.verify_api_key (text) TO authenticated; GRANT EXECUTE ON FUNCTION keyhippo.current_user_context () TO authenticated; -GRANT EXECUTE ON FUNCTION keyhippo_rbac.assign_role_to_user (uuid, uuid, text) TO authenticated; +GRANT EXECUTE ON FUNCTION keyhippo.revoke_api_key (uuid) TO authenticated; -GRANT EXECUTE ON FUNCTION keyhippo_rbac.set_parent_role (uuid, uuid) TO authenticated; +GRANT EXECUTE ON FUNCTION keyhippo.rotate_api_key (uuid) TO authenticated; -GRANT EXECUTE ON FUNCTION keyhippo_rbac.update_user_claims_cache (uuid) TO authenticated; +GRANT EXECUTE ON FUNCTION keyhippo.authorize (keyhippo.app_permission) TO authenticated, anon; -GRANT EXECUTE ON FUNCTION keyhippo_rbac.user_has_permission (text) TO authenticated; +GRANT EXECUTE ON FUNCTION keyhippo_rbac.create_group (text, text) TO authenticated; -GRANT EXECUTE ON FUNCTION keyhippo_rbac.assign_permission_to_role (uuid, text) TO authenticated; +GRANT EXECUTE ON FUNCTION keyhippo_rbac.create_role (text, text, uuid, keyhippo.app_role) TO authenticated; -GRANT EXECUTE ON FUNCTION keyhippo_abac.set_user_attribute (uuid, text, jsonb) TO authenticated; +GRANT EXECUTE ON FUNCTION keyhippo_rbac.assign_role_to_user (uuid, uuid, uuid) TO authenticated; -GRANT EXECUTE ON FUNCTION keyhippo_abac.check_abac_policy (uuid, jsonb) TO authenticated; - -GRANT EXECUTE ON FUNCTION keyhippo_abac.evaluate_policies (uuid) TO authenticated; - -GRANT EXECUTE ON FUNCTION keyhippo_abac.create_policy (text, text, jsonb) TO authenticated; +GRANT EXECUTE ON FUNCTION keyhippo_rbac.assign_permission_to_role (uuid, keyhippo.app_permission) TO authenticated; -- Grant SELECT, INSERT, UPDATE, DELETE on tables GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA keyhippo TO authenticated; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA keyhippo_rbac TO authenticated; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA keyhippo_abac TO authenticated; - -- Revoke all permissions on api_key_secrets from authenticated users REVOKE ALL ON TABLE keyhippo.api_key_secrets FROM authenticated; @@ -1589,156 +792,194 @@ GRANT ALL ON ALL TABLES IN SCHEMA keyhippo TO service_role; GRANT ALL ON ALL TABLES IN SCHEMA keyhippo_rbac TO service_role; -GRANT ALL ON ALL TABLES IN SCHEMA keyhippo_abac TO service_role; +-- Grant necessary permissions for impersonation +GRANT USAGE ON SCHEMA keyhippo_impersonation TO postgres, authenticated, anon; --- Create a trigger to update claims cache when user_group_roles change -CREATE OR REPLACE FUNCTION keyhippo_rbac.trigger_update_claims_cache () - RETURNS TRIGGER - LANGUAGE plpgsql - SECURITY DEFINER - AS $$ -BEGIN - PERFORM - keyhippo_rbac.update_user_claims_cache (NEW.user_id); - RETURN NEW; -END; -$$; +GRANT ALL ON TABLE keyhippo_impersonation.impersonation_state TO postgres; -CREATE TRIGGER after_user_group_roles_change - AFTER INSERT OR UPDATE OR DELETE ON keyhippo_rbac.user_group_roles - FOR EACH ROW - EXECUTE FUNCTION keyhippo_rbac.trigger_update_claims_cache (); +GRANT EXECUTE ON PROCEDURE keyhippo_impersonation.login_as_user (UUID) TO postgres; --- Notify PostgREST to reload configuration -NOTIFY pgrst, -'reload config'; +GRANT EXECUTE ON PROCEDURE keyhippo_impersonation.login_as_anon () TO postgres; --- TODO: Break this out: -DO $$ -BEGIN - -- Insert Admin Group if not already present - IF NOT EXISTS ( - SELECT - 1 - FROM - keyhippo_rbac.groups - WHERE - name = 'Admin Group') THEN - INSERT INTO keyhippo_rbac.groups (name, description) - VALUES ('Admin Group', 'Group for administrators'); -END IF; -END -$$; +GRANT EXECUTE ON PROCEDURE keyhippo_impersonation.logout () TO authenticated, anon; + +GRANT SELECT, DELETE ON keyhippo_impersonation.impersonation_state TO anon; -DO $$ +-- Initialization function +CREATE OR REPLACE FUNCTION keyhippo.initialize_keyhippo () + RETURNS VOID + LANGUAGE plpgsql + SECURITY DEFINER + AS $$ DECLARE admin_group_id uuid; + admin_role_id uuid; + user_group_id uuid; + user_role_id uuid; BEGIN - -- Fetch Admin Group ID + -- Create default groups + INSERT INTO keyhippo_rbac.groups (name, description) + VALUES ('Admin Group', 'Group for administrators'), + ('User Group', 'Group for regular users') + ON CONFLICT (name) + DO NOTHING; + -- Fetch group IDs SELECT id INTO admin_group_id FROM keyhippo_rbac.groups WHERE name = 'Admin Group'; - -- Insert Admin Role if it doesn't exist - IF NOT EXISTS ( - SELECT - 1 - FROM - keyhippo_rbac.roles - WHERE - name = 'Admin' - AND group_id = admin_group_id) THEN - INSERT INTO keyhippo_rbac.roles (name, description, group_id) - VALUES ('Admin', 'Admin role', admin_group_id); -END IF; -END + SELECT + id INTO user_group_id + FROM + keyhippo_rbac.groups + WHERE + name = 'User Group'; + -- Create default roles + INSERT INTO keyhippo_rbac.roles (name, description, group_id, role_type) + VALUES ('Admin', 'Administrator role', admin_group_id, 'admin'), + ('User', 'Regular user role', user_group_id, 'user') + ON CONFLICT (name, group_id) + DO NOTHING; + -- Fetch role IDs + SELECT + id INTO admin_role_id + FROM + keyhippo_rbac.roles + WHERE + name = 'Admin' + AND group_id = admin_group_id; + SELECT + id INTO user_role_id + FROM + keyhippo_rbac.roles + WHERE + name = 'User' + AND group_id = user_group_id; + -- Create default permissions + INSERT INTO keyhippo_rbac.permissions (name, description) + VALUES ('manage_groups', 'Manage groups'), + ('manage_roles', 'Manage roles'), + ('manage_permissions', 'Manage permissions'), + ('manage_scopes', 'Manage scopes'), + ('manage_api_keys', 'Manage API keys'), + ('manage_user_attributes', 'Manage user attributes') + ON CONFLICT (name) + DO NOTHING; + -- Assign all permissions to the Admin role + INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id) + SELECT + admin_role_id, + id + FROM + keyhippo_rbac.permissions + ON CONFLICT (role_id, + permission_id) + DO NOTHING; + -- Assign manage_api_keys permission to the User role + INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id) + SELECT + user_role_id, + id + FROM + keyhippo_rbac.permissions + WHERE + name = 'manage_api_keys' + ON CONFLICT (role_id, + permission_id) + DO NOTHING; + -- Create a default scope + INSERT INTO keyhippo.scopes (name, description) + VALUES ('default', 'Default scope for API keys') + ON CONFLICT (name) + DO NOTHING; +END; $$; -DO $$ +-- Function to assign the default user role to a new user +CREATE OR REPLACE FUNCTION keyhippo.assign_default_role () + RETURNS TRIGGER + LANGUAGE plpgsql + SECURITY DEFINER + AS $$ +DECLARE + user_group_id uuid; + user_role_id uuid; BEGIN - -- Insert User Group if not already present - IF NOT EXISTS ( - SELECT - 1 - FROM - keyhippo_rbac.groups - WHERE - name = 'User Group') THEN - INSERT INTO keyhippo_rbac.groups (name, description) - VALUES ('User Group', 'Group for regular users'); -END IF; -END + SELECT + id INTO user_group_id + FROM + keyhippo_rbac.groups + WHERE + name = 'User Group'; + SELECT + id INTO user_role_id + FROM + keyhippo_rbac.roles + WHERE + name = 'User' + AND group_id = user_group_id; + INSERT INTO keyhippo_rbac.user_group_roles (user_id, group_id, role_id) + VALUES (NEW.id, user_group_id, user_role_id) + ON CONFLICT (user_id, group_id, role_id) + DO NOTHING; + RETURN NEW; +END; $$; -DO $$ +-- Create a trigger to assign the default role to new users +CREATE TRIGGER assign_default_role_trigger + AFTER INSERT ON auth.users + FOR EACH ROW + EXECUTE FUNCTION keyhippo.assign_default_role (); + +-- Function to initialize KeyHippo for an existing Supabase project +CREATE OR REPLACE FUNCTION keyhippo.initialize_existing_project () + RETURNS VOID + LANGUAGE plpgsql + SECURITY DEFINER + AS $$ DECLARE + user_record RECORD; user_group_id uuid; + user_role_id uuid; BEGIN - -- Fetch User Group ID + -- Initialize KeyHippo + PERFORM + keyhippo.initialize_keyhippo (); + -- Get the User Group and Role IDs SELECT id INTO user_group_id FROM keyhippo_rbac.groups WHERE name = 'User Group'; - -- Insert User Role if it doesn't exist - IF NOT EXISTS ( - SELECT - 1 - FROM - keyhippo_rbac.roles - WHERE - name = 'User' - AND group_id = user_group_id) THEN - INSERT INTO keyhippo_rbac.roles (name, description, group_id) - VALUES ('User', 'User role', user_group_id); -END IF; -END + SELECT + id INTO user_role_id + FROM + keyhippo_rbac.roles + WHERE + name = 'User' + AND group_id = user_group_id; + -- Assign the default role to all existing users + FOR user_record IN + SELECT + id + FROM + auth.users LOOP + INSERT INTO keyhippo_rbac.user_group_roles (user_id, group_id, role_id) + VALUES (user_record.id, user_group_id, user_role_id) + ON CONFLICT (user_id, group_id, role_id) + DO NOTHING; + END LOOP; +END; $$; -INSERT INTO keyhippo_rbac.roles (name, description, group_id) - VALUES ('supabase_auth_admin', 'Role for Supabase Auth Admin', ( - SELECT - id - FROM - keyhippo_rbac.groups - WHERE - name = 'Admin Group')); - -INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id) +-- Run the initialization function SELECT - r.id, - p.id -FROM - keyhippo_rbac.roles r, - keyhippo_rbac.permissions p -WHERE - r.name = 'supabase_auth_admin' - AND p.name = 'manage_policies'; - -INSERT INTO keyhippo_rbac.permissions (name, description) - VALUES ('manage_user_attributes', 'Permission to manage user attributes') -ON CONFLICT (name) - DO NOTHING; - -INSERT INTO keyhippo_rbac.permissions (name, description) - VALUES ('rotate_api_key', 'Permission to rotate API keys') -ON CONFLICT (name) - DO NOTHING; - --- Assign manage_user_attributes permission to supabase_auth_admin role -INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id) -SELECT - r.id, - p.id -FROM - keyhippo_rbac.roles r, - keyhippo_rbac.permissions p -WHERE - r.name = 'supabase_auth_admin' - AND p.name = 'manage_user_attributes' -ON CONFLICT (role_id, - permission_id) - DO NOTHING; + keyhippo.initialize_keyhippo (); + +-- Notify PostgREST to reload configuration +NOTIFY pgrst, +'reload config';