{-| 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