diff --git a/redash/query_runner/pg.py b/redash/query_runner/pg.py index de7385f823..229a919777 100644 --- a/redash/query_runner/pg.py +++ b/redash/query_runner/pg.py @@ -254,9 +254,15 @@ def _get_tables(self, schema): def _gen_role_pass(self, role_name: str) -> str: """ - Generate a password for a given role using the datasource secret and role name. + Generate a password for a given role using the shared RLS secret and role name. + + Must match stacklet.shared.sql.rls.gen_role_pass in the platform. """ - secret = settings.DATASOURCE_SECRET_KEY + secret = settings.RLS_SECRET + if not secret: + raise ValueError( + f"REDASH_RLS_SECRET is not set, so the password for role {role_name} can't be derived" + ) return hashlib.sha256(f"{secret}:{role_name}".encode("utf-8")).hexdigest() @inject_iam_auth diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py index 9ed74113a4..78e0c38d1c 100644 --- a/redash/settings/__init__.py +++ b/redash/settings/__init__.py @@ -68,6 +68,11 @@ # The secret key to use when encrypting data source options DATASOURCE_SECRET_KEY = os.environ.get("REDASH_SECRET_KEY", SECRET_KEY) +# The platform's shared RLS secret, from which per-role Postgres passwords are derived. +# Deliberately separate from DATASOURCE_SECRET_KEY, which must keep decrypting stored +# data source options. +RLS_SECRET = os.environ.get("REDASH_RLS_SECRET") + # Whether and how to redirect non-HTTP requests to HTTPS. Disabled by default. ENFORCE_HTTPS = parse_boolean(os.environ.get("REDASH_ENFORCE_HTTPS", "false")) ENFORCE_HTTPS_PERMANENT = parse_boolean(os.environ.get("REDASH_ENFORCE_HTTPS_PERMANENT", "false")) diff --git a/tests/query_runner/test_pg.py b/tests/query_runner/test_pg.py index e72001b0a9..1206d0f4f7 100644 --- a/tests/query_runner/test_pg.py +++ b/tests/query_runner/test_pg.py @@ -1,6 +1,7 @@ from unittest import TestCase +from unittest.mock import patch -from redash.query_runner.pg import build_schema +from redash.query_runner.pg import PostgreSQL, build_schema class TestBuildSchema(TestCase): @@ -41,3 +42,30 @@ def test_build_schema_with_data_types(self): self.assertListEqual( schema["main.users"]["columns"], [{"name": "id", "type": "integer"}, {"name": "name", "type": "varchar"}] ) + + +class TestGenRolePass(TestCase): + # The same vector is pinned against stacklet.shared.sql.rls.gen_role_pass in the + # platform; the two must derive identical passwords. + ROLE = "sso_alice_at_example_com_a1b2c3" + SECRET = "rls-test-secret" + EXPECTED = "30f1f1a4d09bae826d882a048b1ea7bcc6354e710dd4d7faf4aefd55e07913c9" + + def setUp(self): + self.runner = PostgreSQL({}) + + def test_matches_platform_vector(self): + with patch("redash.settings.RLS_SECRET", self.SECRET): + self.assertEqual(self.runner._gen_role_pass(self.ROLE), self.EXPECTED) + + def test_ignores_datasource_secret_key(self): + with ( + patch("redash.settings.RLS_SECRET", self.SECRET), + patch("redash.settings.DATASOURCE_SECRET_KEY", "something-else"), + ): + self.assertEqual(self.runner._gen_role_pass(self.ROLE), self.EXPECTED) + + def test_unset_secret_raises(self): + with patch("redash.settings.RLS_SECRET", None): + with self.assertRaisesRegex(ValueError, "REDASH_RLS_SECRET"): + self.runner._gen_role_pass(self.ROLE)