Skip to content

Commit

Permalink
fixup! refactor(extension): begin replacing JWTs with salted hashes
Browse files Browse the repository at this point in the history
  • Loading branch information
david-r-cox committed Sep 25, 2024
1 parent 4fd5229 commit fc1f5a8
Showing 1 changed file with 155 additions and 71 deletions.
226 changes: 155 additions & 71 deletions extension/keyhippo--0.0.40.sql
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ BEGIN
RAISE EXCEPTION '[KeyHippo] Invalid key description';
END IF;
-- Generate a secure random API key
new_api_key := encode(extensions.gen_random_bytes(32), 'base64');
new_api_key := encode(extensions.gen_random_bytes(64), 'base64');
new_api_key_id := extensions.gen_random_uuid ();
-- Store the hashed key
INSERT INTO keyhippo.api_keys (id, user_id, key_hash, description)
VALUES (new_api_key_id, authenticated_user_id, extensions.crypt(new_api_key, extensions.gen_salt('bf')), key_description);
VALUES (new_api_key_id, authenticated_user_id, extensions.crypt(new_api_key, extensions.gen_salt('bf', 8)), key_description);
-- Return the API key and its ID
RETURN QUERY
SELECT
Expand Down Expand Up @@ -248,10 +248,10 @@ CREATE POLICY "Allow user to update their own API keys" ON keyhippo.api_keys
-- RBAC + ABAC Implementation
-- ================================================================
-- Create RBAC Schema
CREATE SCHEMA IF NOT EXISTS keyhippo_rbac AUTHORIZATION postgres;
CREATE SCHEMA IF NOT EXISTS keyhippo_rbac;

-- Create ABAC Schema
CREATE SCHEMA IF NOT EXISTS keyhippo_abac AUTHORIZATION postgres;
CREATE SCHEMA IF NOT EXISTS keyhippo_abac;

-- -------------------------------
-- RBAC Tables
Expand All @@ -266,10 +266,11 @@ CREATE TABLE IF NOT EXISTS keyhippo_rbac.groups (
-- Create Roles Table
CREATE TABLE IF NOT EXISTS keyhippo_rbac.roles (
id uuid PRIMARY KEY DEFAULT extensions.gen_random_uuid (),
name text UNIQUE NOT NULL,
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
parent_role_id uuid REFERENCES keyhippo_rbac.roles (id) ON DELETE SET NULL,
UNIQUE (name, group_id)
);

-- Create Permissions Table
Expand Down Expand Up @@ -314,71 +315,86 @@ CREATE TABLE IF NOT EXISTS keyhippo_abac.policies (
id uuid PRIMARY KEY DEFAULT extensions.gen_random_uuid (),
name text UNIQUE NOT NULL,
description text,
policy JSONB NOT NULL
policy jsonb NOT NULL
);

-- -------------------------------
-- RBAC Functions
-- -------------------------------
-- Function: add_user_to_group
CREATE OR REPLACE FUNCTION keyhippo_rbac.add_user_to_group (p_user_id uuid, p_group_id uuid, p_role_name text)
RETURNS VOID
-- Function: assign_role_to_user
CREATE OR REPLACE FUNCTION keyhippo_rbac.assign_role_to_user (p_user_id uuid, p_group_id uuid, p_role_name text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_temp
AS $$
DECLARE
v_role_id uuid;
BEGIN
-- Fetch the role ID based on role name and group
-- 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 NOT FOUND THEN
RAISE EXCEPTION 'Role "%" not found in group ID %', p_role_name, 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 into user_group_roles if not exists
-- 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
DO NOTHING;
-- Update user claims cache
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;
$$;

-- Function: update_user_claims_cache
CREATE OR REPLACE FUNCTION keyhippo_rbac.update_user_claims_cache (p_user_id uuid)
RETURNS VOID
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_temp
AS $$
DECLARE
v_claims jsonb := '{}'::jsonb;
v_group_id uuid;
v_role_names text[];
BEGIN
-- Delete existing claims for the user
DELETE FROM keyhippo_rbac.claims_cache
WHERE user_id = p_user_id;
INSERT INTO keyhippo_rbac.claims_cache (user_id, rbac_claims)
-- Gather roles for each group
FOR v_group_id,
v_role_names IN
SELECT
p_user_id,
jsonb_object_agg(group_id::text, roles)
FROM (
SELECT
ugr.group_id,
jsonb_agg(DISTINCT r.name) AS roles
FROM
keyhippo_rbac.user_group_roles ugr
JOIN keyhippo_rbac.roles r ON ugr.role_id = r.id
WHERE
ugr.user_id = p_user_id
GROUP BY
ugr.group_id) AS group_roles
GROUP BY
p_user_id;
g.id,
array_agg(DISTINCT r.name)
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 LOOP
v_claims := v_claims || jsonb_build_object(v_group_id::text, v_role_names);
END LOOP;
-- Insert new claims
INSERT INTO keyhippo_rbac.claims_cache (user_id, rbac_claims)
VALUES (p_user_id, v_claims);
-- If no claims were inserted (user has no roles), ensure an empty claims cache entry exists
IF NOT FOUND THEN
INSERT INTO keyhippo_rbac.claims_cache (user_id, rbac_claims)
VALUES (p_user_id, '{}')
ON CONFLICT (user_id)
DO NOTHING;
END IF;
END;
$$;

Expand All @@ -401,7 +417,76 @@ BEGIN
END;
$$;

GRANT EXECUTE ON FUNCTION keyhippo_abac.set_user_attribute (uuid, text, jsonb) TO authenticated;
CREATE OR REPLACE FUNCTION keyhippo_abac.create_policy (p_name text, p_description text, p_policy jsonb)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_temp
AS $$
BEGIN
INSERT INTO keyhippo_abac.policies (name, description, POLICY)
VALUES (p_name, p_description, p_policy)
ON CONFLICT (name)
DO UPDATE SET
description = EXCLUDED.description, POLICY = EXCLUDED.policy;
END;
$$;

-- Function: check_abac_policy
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 $$
DECLARE
v_attribute_value jsonb;
v_policy_attribute text;
v_policy_value jsonb;
BEGIN
v_policy_attribute := p_policy ->> 'attribute';
v_policy_value := p_policy -> 'value';
SELECT
attributes -> v_policy_attribute INTO v_attribute_value
FROM
keyhippo_abac.user_attributes
WHERE
user_id = p_user_id;
IF v_attribute_value IS NULL THEN
RETURN FALSE;
END IF;
RETURN CASE WHEN p_policy ->> 'type' = 'attribute_equals' THEN
v_attribute_value = v_policy_value
WHEN p_policy ->> 'type' = 'attribute_contains' THEN
v_attribute_value @> v_policy_value
ELSE
FALSE
END;
END;
$$;

-- Function: evaluate_policies
CREATE OR REPLACE FUNCTION keyhippo_abac.evaluate_policies (p_user_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_temp
AS $$
DECLARE
policy_record RECORD;
BEGIN
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;
$$;

-- -------------------------------
-- RLS Policies
Expand Down Expand Up @@ -447,45 +532,48 @@ CREATE POLICY "abac_user_attributes_access" ON keyhippo_abac.user_attributes
-- -------------------------------
-- Permissions and Grants
-- -------------------------------
-- Grant USAGE on RBAC and ABAC Schemas to Authenticated Role
-- Grant USAGE on schemas
GRANT USAGE ON SCHEMA keyhippo_rbac TO authenticated;

GRANT USAGE ON SCHEMA keyhippo_abac TO authenticated;

-- Grant SELECT, INSERT, UPDATE, DELETE on All RBAC Tables to Authenticated Role
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA keyhippo_rbac TO authenticated;
-- Grant SELECT on RBAC, ABAC tables
GRANT SELECT ON ALL TABLES IN SCHEMA keyhippo_rbac TO authenticated;

-- Grant SELECT, INSERT, UPDATE, DELETE on All ABAC Tables to Authenticated Role
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA keyhippo_abac TO authenticated;
GRANT SELECT ON ALL TABLES IN SCHEMA keyhippo_abac TO authenticated;

-- Grant EXECUTE on RBAC Functions to Authenticated Role
GRANT EXECUTE ON FUNCTION keyhippo_rbac.add_user_to_group (uuid, uuid, text) TO authenticated;
-- Grant EXECUTE on RBAC functions
GRANT EXECUTE ON FUNCTION keyhippo_rbac.assign_role_to_user (uuid, uuid, text) TO authenticated;

GRANT EXECUTE ON FUNCTION keyhippo_rbac.update_user_claims_cache (uuid) TO authenticated;

-- Grant EXECUTE on ABAC Functions to Authenticated Role
-- Grant EXECUTE on ABAC functions
GRANT EXECUTE ON FUNCTION keyhippo_abac.set_user_attribute (uuid, text, jsonb) TO authenticated;

GRANT EXECUTE ON FUNCTION keyhippo_abac.create_policy (text, text, jsonb) 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;

-- -------------------------------
-- Indexes for Performance
-- -------------------------------
-- GIN Index on user_attributes.attributes
CREATE INDEX IF NOT EXISTS idx_user_attributes_attributes ON keyhippo_abac.user_attributes USING GIN (attributes);
CREATE INDEX IF NOT EXISTS idx_user_attributes_gin ON keyhippo_abac.user_attributes USING gin (attributes);

-- GIN Index on claims_cache.rbac_claims
CREATE INDEX IF NOT EXISTS idx_claims_cache_rbac_claims ON keyhippo_rbac.claims_cache USING GIN (rbac_claims);
CREATE INDEX IF NOT EXISTS idx_claims_cache_gin ON keyhippo_rbac.claims_cache USING gin (rbac_claims);

-- -------------------------------
-- Default Data Insertion
-- -------------------------------
-- Inserting Default Groups
-- Insert default groups
INSERT INTO keyhippo_rbac.groups (name, description)
VALUES ('Admin Group', 'Group with administrative privileges'),
('User Group', 'Group with standard user privileges')
ON CONFLICT (name)
DO NOTHING;

-- Inserting Default Roles
-- Insert default roles
INSERT INTO keyhippo_rbac.roles (name, description, group_id)
SELECT
'Admin',
Expand All @@ -495,7 +583,8 @@ FROM
keyhippo_rbac.groups
WHERE
name = 'Admin Group'
ON CONFLICT (name)
ON CONFLICT (name,
group_id)
DO NOTHING;

INSERT INTO keyhippo_rbac.roles (name, description, group_id)
Expand All @@ -507,70 +596,65 @@ FROM
keyhippo_rbac.groups
WHERE
name = 'User Group'
ON CONFLICT (name)
ON CONFLICT (name,
group_id)
DO NOTHING;

-- Inserting Default Permissions
-- Insert default permissions
INSERT INTO keyhippo_rbac.permissions (name, description)
VALUES ('read', 'Read Permission'),
('write', 'Write Permission'),
('delete', 'Delete Permission')
('delete', 'Delete Permission'),
('manage_policies', 'Manage ABAC Policies')
ON CONFLICT (name)
DO NOTHING;

-- Mapping Roles to Permissions
-- Admin Role: All permissions
-- Assign permissions to roles
INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id)
SELECT
r.id,
p.id
FROM
keyhippo_rbac.roles r
JOIN keyhippo_rbac.permissions p ON p.name IN ('read', 'write', 'delete')
CROSS JOIN keyhippo_rbac.permissions p
WHERE
r.name = 'Admin'
ON CONFLICT
DO NOTHING;

-- User Role: read and write permissions
INSERT INTO keyhippo_rbac.role_permissions (role_id, permission_id)
SELECT
r.id,
p.id
FROM
keyhippo_rbac.roles r
JOIN keyhippo_rbac.permissions p ON p.name IN ('read', 'write')
CROSS JOIN keyhippo_rbac.permissions p
WHERE
r.name = 'User'
AND p.name IN ('read', 'write')
ON CONFLICT
DO NOTHING;

-- -------------------------------
-- Triggers for Automatic Claims Cache Updates
-- Triggers
-- -------------------------------
-- Function: trigger_update_claims_cache
CREATE OR REPLACE FUNCTION keyhippo_rbac.trigger_update_claims_cache ()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_temp
AS $$
BEGIN
PERFORM
keyhippo_rbac.update_user_claims_cache (NEW.user_id);
RETURN NEW;
END;
$$;
$$
LANGUAGE plpgsql
SECURITY DEFINER;

-- Trigger: after_insert_update_user_group_roles
CREATE TRIGGER after_insert_update_user_group_roles
AFTER INSERT OR UPDATE ON keyhippo_rbac.user_group_roles
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 ();

-- -------------------------------
-- Notifications and Final Setup
-- -------------------------------
-- Notify pgRest to reload configuration if necessary
-- Notify pgRest to reload configuration
NOTIFY pgrst,
'reload config';

0 comments on commit fc1f5a8

Please sign in to comment.