Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`defaults security configurations configuration verification should crea
{
"current_user": "postgres",
"database_name": "test-database",
"default_func_acl_count": "2",
"default_func_acl_count": "1",
"public_db_connect": false,
"public_db_create": false,
"public_schema_create": false,
Expand Down
58 changes: 58 additions & 0 deletions packages/defaults/__tests__/defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,64 @@ describe('defaults security configurations', () => {
});
});

describe('schema public function grants', () => {
beforeEach(async () => {
await pg.any(`
CREATE FUNCTION public_grant_probe()
RETURNS text AS $$
BEGIN
RETURN 'probe';
END;
$$ LANGUAGE plpgsql;
`);
});

it('grants no role schema-wide execution, not even authenticated', async () => {
const [privileges] = await pg.any(`
SELECT
has_function_privilege('authenticated', 'public_grant_probe()', 'execute') as authenticated_can_execute,
has_function_privilege('administrator', 'public_grant_probe()', 'execute') as administrator_can_execute,
has_function_privilege('anonymous', 'public_grant_probe()', 'execute') as anonymous_can_execute
`);

// A function in schema public is reachable only by an explicit grant.
expect(privileges.authenticated_can_execute).toBe(false);
expect(privileges.administrator_can_execute).toBe(false);
expect(privileges.anonymous_can_execute).toBe(false);
});

it('leaves an explicit grant as the only way in', async () => {
await pg.any(`GRANT EXECUTE ON FUNCTION public_grant_probe() TO authenticated`);

const [privileges] = await pg.any(`
SELECT
has_function_privilege('authenticated', 'public_grant_probe()', 'execute') as authenticated_can_execute,
has_function_privilege('anonymous', 'public_grant_probe()', 'execute') as anonymous_can_execute
`);

expect(privileges.authenticated_can_execute).toBe(true);
expect(privileges.anonymous_can_execute).toBe(false);
});
});

describe('extension ordering', () => {
// The revoke is prospective, so an extension created after this module
// loses PUBLIC's EXECUTE and its functions become uncallable: pgpm-defaults
// must be the last requirement of any deployment that installs extensions.
it('leaves an extension created after it without PUBLIC execution', async () => {
await pg.any(`CREATE EXTENSION IF NOT EXISTS citext`);

const [privileges] = await pg.any(`
SELECT
has_function_privilege('public', 'citext_eq(citext,citext)', 'execute') as public_can_execute,
has_function_privilege('anonymous', 'citext_eq(citext,citext)', 'execute') as anonymous_can_execute
`);

expect(privileges.public_can_execute).toBe(false);
expect(privileges.anonymous_can_execute).toBe(false);
});
});

describe('privilege inheritance', () => {
it('should verify that new schemas inherit secure defaults', async () => {
// Create a new schema
Expand Down
8 changes: 5 additions & 3 deletions packages/defaults/deploy/defaults/public.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ $$;
-- NOTE: don't alter this as new schemas inherit this behavior
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO authenticated, anonymous, administrator;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT EXECUTE ON FUNCTIONS TO authenticated, anonymous, administrator;
-- No schema-wide function grants here. Schema public holds only extension
-- functions (which carry PUBLIC=X from before the revoke above) and the
-- pgpm-verify deploy-time helpers, so granting a role every function in it
-- adds nothing but a blanket grant. Application function grants are declared
-- per function.
COMMIT;
13 changes: 12 additions & 1 deletion packages/defaults/revert/defaults/public.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

BEGIN;

-- XXX Add DDLs here.
-- Restores PostgreSQL's out-of-the-box PUBLIC privileges.
GRANT CREATE ON SCHEMA public TO PUBLIC;
ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC;
DO $$
DECLARE
sql text;
BEGIN
SELECT
format('GRANT CONNECT, TEMPORARY ON DATABASE %I TO PUBLIC', current_database()) INTO sql;
EXECUTE sql;
END
$$;

COMMIT;
Binary file modified packages/defaults/sql/pgpm-defaults--0.35.0.bundle.tar.gz
Binary file not shown.
7 changes: 1 addition & 6 deletions packages/defaults/sql/pgpm-defaults--0.35.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,4 @@ $EOFCODE$;
ALTER DEFAULT PRIVILEGES
REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC RESTRICT;

REVOKE CREATE ON SCHEMA public FROM PUBLIC RESTRICT;

GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO authenticated, anonymous, administrator;

ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT EXECUTE ON FUNCTIONS TO authenticated, anonymous, administrator;
REVOKE CREATE ON SCHEMA public FROM PUBLIC RESTRICT;
50 changes: 49 additions & 1 deletion packages/defaults/verify/defaults/public.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

BEGIN;

-- XXX Add verifications here.
DO $$
DECLARE
public_grantee constant oid := 0;
BEGIN
IF has_database_privilege('public', current_database(), 'CREATE') THEN
RAISE EXCEPTION 'PUBLIC still holds CREATE on database %', current_database();
END IF;

IF has_schema_privilege('public', 'public', 'CREATE') THEN
RAISE EXCEPTION 'PUBLIC still holds CREATE on schema public';
END IF;

-- The database-wide default: PUBLIC must not inherit EXECUTE on functions
-- created from here on. Only the role-scoped, schema-less form does this;
-- an IN SCHEMA variant would leave every other schema untouched.
IF NOT EXISTS (
SELECT 1
FROM pg_default_acl
WHERE defaclnamespace = 0
AND defaclobjtype = 'f'
AND current_user::regrole::oid = defaclrole
) THEN
RAISE EXCEPTION 'no database-wide default privileges for functions owned by %', current_user;
END IF;

IF EXISTS (
SELECT 1
FROM pg_default_acl d, aclexplode(d.defaclacl) a
WHERE d.defaclnamespace = 0
AND d.defaclobjtype = 'f'
AND a.grantee = public_grantee
AND a.privilege_type = 'EXECUTE'
) THEN
RAISE EXCEPTION 'PUBLIC still holds a database-wide EXECUTE default on functions';
END IF;

-- Schema public carries no schema-wide function default at all: a role that
-- needs a function there is granted that function.
IF EXISTS (
SELECT 1
FROM pg_default_acl d, aclexplode(d.defaclacl) a
WHERE d.defaclnamespace = 'public'::regnamespace
AND d.defaclobjtype = 'f'
AND a.privilege_type = 'EXECUTE'
) THEN
RAISE EXCEPTION 'schema public has a schema-wide EXECUTE default on functions';
END IF;
END
$$;

ROLLBACK;
Loading