packages feed

ihp-1.4.0: IHP/DataSync/Role.hs

{-|
Module: IHP.DataSync.Role
Description: Postgres role management for RLS
Copyright: (c) digitally induced GmbH, 2021

The default user that creates a table in postgres always
has access to all rows inside the table. The default user is not restricted
to the RLS policies.

Therefore we need to use a second role whenever we want to
make a query with RLS enabled. Basically for every query we do, we'll
wrap it in a transaction and then use 'SET LOCAL ROLE ..' to switch to
our second role for the duration of the transaction.

-}
module IHP.DataSync.Role where

import IHP.Prelude
import IHP.FrameworkConfig
import IHP.ModelSupport
import qualified Database.PostgreSQL.Simple.Types as PG

doesRoleExists :: (?modelContext :: ModelContext) => Text -> IO Bool
doesRoleExists name = sqlQueryScalar "SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = ? LIMIT 1)" [name]

ensureAuthenticatedRoleExists :: (?context :: context, ConfigProvider context, ?modelContext :: ModelContext) => IO ()
ensureAuthenticatedRoleExists = do
    roleExists <- doesRoleExists authenticatedRole
    unless roleExists (createAuthenticatedRole authenticatedRole)
    grantPermissions authenticatedRole

createAuthenticatedRole :: (?modelContext :: ModelContext) => Text -> IO ()
createAuthenticatedRole role = do
    -- The role is only going to be used from 'SET ROLE ..' calls
    -- Therefore we can disallow direct connection with NOLOGIN
    sqlExec "CREATE ROLE ? NOLOGIN" [PG.Identifier role]


    pure ()

grantPermissions :: (?modelContext :: ModelContext) => Text -> IO ()
grantPermissions role = do
    -- From SO https://stackoverflow.com/a/17355059/14144232
    --
    -- GRANTs on different objects are separate. GRANTing on a database doesn't GRANT rights to the schema within. Similiarly, GRANTing on a schema doesn't grant rights on the tables within.
    -- 
    -- If you have rights to SELECT from a table, but not the right to see it in the schema that contains it then you can't access the table.
    -- 
    -- The rights tests are done in order:
    -- 
    -- Do you have `USAGE` on the schema? 
    --     No:  Reject access. 
    --     Yes: Do you also have the appropriate rights on the table? 
    --         No:  Reject access. 
    --         Yes: Check column privileges.

    -- The role should have access to all existing tables in our schema
    sqlExec "GRANT USAGE ON SCHEMA public TO ?" [PG.Identifier role]
    
    -- The role should have access to all existing tables in our schema
    sqlExec "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ?" [PG.Identifier role]

    -- Also grant access to all tables created in the future
    sqlExec "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO ?" [PG.Identifier role]

    pure ()

authenticatedRole :: (?context :: context, ConfigProvider context) => Text
authenticatedRole = ?context.frameworkConfig.rlsAuthenticatedRole