Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dropping role with table privileges granted by non-neon_superuser #10964

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion compute_tools/src/spec_apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ async fn get_operations<'a>(
Operation {
query: format!(
include_str!("sql/pre_drop_role_revoke_privileges.sql"),
role_name = quoted,
role_name = op.name,
),
comment: None,
},
Expand Down
24 changes: 18 additions & 6 deletions compute_tools/src/sql/pre_drop_role_revoke_privileges.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
SET SESSION ROLE neon_superuser;
-- Any time you see {role_name}, remember that this SQL script gets embedded
-- into compute_ctl, and is run with a format!(), so {role_name} is a Rust
-- variable.

DO $$
DECLARE
schema TEXT;
grantor TEXT;
revoke_query TEXT;
BEGIN
FOR schema IN
Expand All @@ -15,12 +18,21 @@ BEGIN
-- ii) it's easy to add more schemas to the list if needed.
WHERE schema_name IN ('public')
LOOP
revoke_query := format(
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA %I FROM {role_name} GRANTED BY neon_superuser;',
schema
);
FOR grantor IN
SELECT DISTINCT rtg.grantor
FROM information_schema.role_table_grants AS rtg
WHERE grantee = '{role_name}'
LOOP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, isn't this prone to SQL-injection now? I.e. if you name role like ';DROP DATABASE template0;'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think you're right. Hmmm

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spent some time with it. I think here you need to pass the result of escape_literal(), see another helper. I while ago I added it, and it worked well in my tests, but please double check

EXECUTE format('SET LOCAL ROLE %I', grantor);

EXECUTE revoke_query;
revoke_query := format(
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA %I FROM "{role_name}" GRANTED BY %I',
schema,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, if the grantee is some 'funny' name, I guess you need something like quote_ident() or so

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here, you need to use the result of a new helper I added here #11045 + remove quotes, just {role_name}

grantor
);

EXECUTE revoke_query;
END LOOP;
END LOOP;
END;
$$;
Expand Down
69 changes: 67 additions & 2 deletions test_runner/regress/test_compute_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,12 @@ def test_dropdb_with_subscription(neon_simple_env: NeonEnv):
assert curr_db[0] == "publisher_db"


def test_compute_drop_role(neon_simple_env: NeonEnv):
def test_drop_role_with_table_privileges_from_neon_superuser(neon_simple_env: NeonEnv):
"""
Test that compute_ctl can drop a role even if it has some depending objects
like permissions in one of the databases.
like permissions in one of the databases that were granted by
neon_superuser.

Reproduction test for /~https://github.com/neondatabase/cloud/issues/13582
"""
env = neon_simple_env
Expand Down Expand Up @@ -370,3 +372,66 @@ def test_compute_drop_role(neon_simple_env: NeonEnv):
cursor.execute("SELECT rolname FROM pg_roles WHERE rolname = 'readonly2'")
role = cursor.fetchone()
assert role is None


def test_drop_role_with_table_privileges_from_non_neon_superuser(neon_simple_env: NeonEnv):
"""
Test that compute_ctl can drop a role if the role has previously been
granted table privileges by a role other than neon_superuser.
"""
TEST_DB_NAME = "neondb"
TEST_GRANTOR = "my_grantor"
TEST_GRANTEE = "my_grantee"
Comment on lines +383 to +384
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless of whether the current code is prone to injections or not (I bet it does, though), let's make role names for both grantor and grantee to contain the injections vector like '; RAISE 'ouch'; ' or "; RAISE 'ouch'; " depending on the type of quotes you use in what case

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.


env = neon_simple_env

endpoint = env.endpoints.create_start("main")
endpoint.respec_deep(
**{
"skip_pg_catalog_updates": False,
"cluster": {
"roles": [
{
# We need to create role via compute_ctl, because in this case it will receive
# additional grants equivalent to our real environment, so we can repro some
# issues.
"name": TEST_GRANTOR,
# Some autocomplete-suggested hash, no specific meaning.
"encrypted_password": "SCRAM-SHA-256$4096:hBT22QjqpydQWqEulorfXA==$miBogcoj68JWYdsNB5PW1X6PjSLBEcNuctuhtGkb4PY=:hxk2gxkwxGo6P7GCtfpMlhA9zwHvPMsCz+NQf2HfvWk=",
"options": [],
},
],
"databases": [
{
"name": TEST_DB_NAME,
"owner": TEST_GRANTOR,
},
],
},
}
)

endpoint.reconfigure()

with endpoint.cursor(dbname=TEST_DB_NAME, user=TEST_GRANTOR) as cursor:
cursor.execute(f'CREATE USER "{TEST_GRANTEE}"')
cursor.execute("CREATE TABLE test_table(id bigint)")
cursor.execute(f'GRANT ALL ON TABLE test_table TO "{TEST_GRANTEE}"')

endpoint.respec_deep(
**{
"skip_pg_catalog_updates": False,
"delta_operations": [
{
"action": "delete_role",
"name": TEST_GRANTEE,
},
],
}
)
endpoint.reconfigure()

with endpoint.cursor() as cursor:
cursor.execute(f"SELECT rolname FROM pg_roles WHERE rolname = '{TEST_GRANTEE}'")
role = cursor.fetchone()
assert role is None
Loading