From 9b4a15a513761ed53a6edf91e0568ba19080dc11 Mon Sep 17 00:00:00 2001 From: Walt Askew Date: Mon, 21 Apr 2025 18:22:03 +0000 Subject: [PATCH 1/3] feat: support fine-grained permissions database roles in connect Add an optional `database_role` argument to `connect` for supplying the database role to connect as when using [fine-grained access controls](https://cloud.google.com/spanner/docs/access-with-fgac) --- README.rst | 6 ++++++ google/cloud/spanner_dbapi/connection.py | 9 ++++++++- tests/unit/spanner_dbapi/test_connect.py | 10 ++++++++-- tests/unit/spanner_dbapi/test_connection.py | 12 +++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 7e75685f2e..982d01c780 100644 --- a/README.rst +++ b/README.rst @@ -252,6 +252,12 @@ Connection API represents a wrap-around for Python Spanner API, written in accor result = cursor.fetchall() +If using [fine-grained access controls](https://cloud.google.com/spanner/docs/access-with-fgac) you can pass a ``database_role`` argument to connect as that role: + +.. code:: python + connection = connect("instance-id", "database-id", database_role='your-role') + + Aborted Transactions Retry Mechanism ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index a615a282b5..dda762af6c 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -720,6 +720,7 @@ def connect( user_agent=None, client=None, route_to_leader_enabled=True, + database_role=None, **kwargs, ): """Creates a connection to a Google Cloud Spanner database. @@ -763,6 +764,10 @@ def connect( disable leader aware routing. Disabling leader aware routing would route all requests in RW/PDML transactions to the closest region. + :type database_role: str + :param database_role: (Optional) The database role to connect as when using + fine-grained access controls. + **kwargs: Initial value for connection variables. @@ -797,7 +802,9 @@ def connect( instance = client.instance(instance_id) database = None if database_id: - database = instance.database(database_id, pool=pool) + database = instance.database( + database_id, pool=pool, database_role=database_role + ) conn = Connection(instance, database) if pool is not None: conn._own_pool = False diff --git a/tests/unit/spanner_dbapi/test_connect.py b/tests/unit/spanner_dbapi/test_connect.py index 30ab3c7a8d..d4bd22b890 100644 --- a/tests/unit/spanner_dbapi/test_connect.py +++ b/tests/unit/spanner_dbapi/test_connect.py @@ -59,7 +59,9 @@ def test_w_implicit(self, mock_client): ) self.assertIs(connection.database, database) - instance.database.assert_called_once_with(DATABASE, pool=None) + instance.database.assert_called_once_with( + DATABASE, pool=None, database_role=None + ) # Datbase constructs its own pool self.assertIsNotNone(connection.database._pool) self.assertTrue(connection.instance._client.route_to_leader_enabled) @@ -75,6 +77,7 @@ def test_w_explicit(self, mock_client): client = mock_client.return_value instance = client.instance.return_value database = instance.database.return_value + role = "some_role" connection = connect( INSTANCE, @@ -82,6 +85,7 @@ def test_w_explicit(self, mock_client): PROJECT, credentials, pool=pool, + database_role=role, user_agent=USER_AGENT, route_to_leader_enabled=False, ) @@ -102,7 +106,9 @@ def test_w_explicit(self, mock_client): client.instance.assert_called_once_with(INSTANCE) self.assertIs(connection.database, database) - instance.database.assert_called_once_with(DATABASE, pool=pool) + instance.database.assert_called_once_with( + DATABASE, pool=pool, database_role=role + ) def test_w_credential_file_path(self, mock_client): from google.cloud.spanner_dbapi import connect diff --git a/tests/unit/spanner_dbapi/test_connection.py b/tests/unit/spanner_dbapi/test_connection.py index 4bee9e93c7..c0f358a3cb 100644 --- a/tests/unit/spanner_dbapi/test_connection.py +++ b/tests/unit/spanner_dbapi/test_connection.py @@ -815,6 +815,13 @@ def test_custom_client_connection(self): connection = connect("test-instance", "test-database", client=client) self.assertTrue(connection.instance._client == client) + def test_custom_database_role(self): + from google.cloud.spanner_dbapi import connect + + role = "some_role" + connection = connect("test-instance", "test-database", database_role=role) + self.assertEqual(connection.database.database_role, role) + def test_invalid_custom_client_connection(self): from google.cloud.spanner_dbapi import connect @@ -858,8 +865,9 @@ def database( database_id="database_id", pool=None, database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + database_role=None, ): - return _Database(database_id, pool, database_dialect) + return _Database(database_id, pool, database_dialect, database_role) class _Database(object): @@ -868,7 +876,9 @@ def __init__( database_id="database_id", pool=None, database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + database_role=None, ): self.name = database_id self.pool = pool self.database_dialect = database_dialect + self.database_role = database_role From b1bb42220702025b6661c31a270f9092fd38157d Mon Sep 17 00:00:00 2001 From: Walt Askew Date: Mon, 21 Apr 2025 18:22:03 +0000 Subject: [PATCH 2/3] feat: support fine-grained permissions database roles in connect Add an optional `database_role` argument to `connect` for supplying the database role to connect as when using [fine-grained access controls](https://cloud.google.com/spanner/docs/access-with-fgac) --- README.rst | 6 ++++++ google/cloud/spanner_dbapi/connection.py | 9 ++++++++- tests/unit/spanner_dbapi/test_connect.py | 10 ++++++++-- tests/unit/spanner_dbapi/test_connection.py | 12 +++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 7e75685f2e..982d01c780 100644 --- a/README.rst +++ b/README.rst @@ -252,6 +252,12 @@ Connection API represents a wrap-around for Python Spanner API, written in accor result = cursor.fetchall() +If using [fine-grained access controls](https://cloud.google.com/spanner/docs/access-with-fgac) you can pass a ``database_role`` argument to connect as that role: + +.. code:: python + connection = connect("instance-id", "database-id", database_role='your-role') + + Aborted Transactions Retry Mechanism ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index a615a282b5..dda762af6c 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -720,6 +720,7 @@ def connect( user_agent=None, client=None, route_to_leader_enabled=True, + database_role=None, **kwargs, ): """Creates a connection to a Google Cloud Spanner database. @@ -763,6 +764,10 @@ def connect( disable leader aware routing. Disabling leader aware routing would route all requests in RW/PDML transactions to the closest region. + :type database_role: str + :param database_role: (Optional) The database role to connect as when using + fine-grained access controls. + **kwargs: Initial value for connection variables. @@ -797,7 +802,9 @@ def connect( instance = client.instance(instance_id) database = None if database_id: - database = instance.database(database_id, pool=pool) + database = instance.database( + database_id, pool=pool, database_role=database_role + ) conn = Connection(instance, database) if pool is not None: conn._own_pool = False diff --git a/tests/unit/spanner_dbapi/test_connect.py b/tests/unit/spanner_dbapi/test_connect.py index 30ab3c7a8d..d4bd22b890 100644 --- a/tests/unit/spanner_dbapi/test_connect.py +++ b/tests/unit/spanner_dbapi/test_connect.py @@ -59,7 +59,9 @@ def test_w_implicit(self, mock_client): ) self.assertIs(connection.database, database) - instance.database.assert_called_once_with(DATABASE, pool=None) + instance.database.assert_called_once_with( + DATABASE, pool=None, database_role=None + ) # Datbase constructs its own pool self.assertIsNotNone(connection.database._pool) self.assertTrue(connection.instance._client.route_to_leader_enabled) @@ -75,6 +77,7 @@ def test_w_explicit(self, mock_client): client = mock_client.return_value instance = client.instance.return_value database = instance.database.return_value + role = "some_role" connection = connect( INSTANCE, @@ -82,6 +85,7 @@ def test_w_explicit(self, mock_client): PROJECT, credentials, pool=pool, + database_role=role, user_agent=USER_AGENT, route_to_leader_enabled=False, ) @@ -102,7 +106,9 @@ def test_w_explicit(self, mock_client): client.instance.assert_called_once_with(INSTANCE) self.assertIs(connection.database, database) - instance.database.assert_called_once_with(DATABASE, pool=pool) + instance.database.assert_called_once_with( + DATABASE, pool=pool, database_role=role + ) def test_w_credential_file_path(self, mock_client): from google.cloud.spanner_dbapi import connect diff --git a/tests/unit/spanner_dbapi/test_connection.py b/tests/unit/spanner_dbapi/test_connection.py index 4bee9e93c7..c0f358a3cb 100644 --- a/tests/unit/spanner_dbapi/test_connection.py +++ b/tests/unit/spanner_dbapi/test_connection.py @@ -815,6 +815,13 @@ def test_custom_client_connection(self): connection = connect("test-instance", "test-database", client=client) self.assertTrue(connection.instance._client == client) + def test_custom_database_role(self): + from google.cloud.spanner_dbapi import connect + + role = "some_role" + connection = connect("test-instance", "test-database", database_role=role) + self.assertEqual(connection.database.database_role, role) + def test_invalid_custom_client_connection(self): from google.cloud.spanner_dbapi import connect @@ -858,8 +865,9 @@ def database( database_id="database_id", pool=None, database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + database_role=None, ): - return _Database(database_id, pool, database_dialect) + return _Database(database_id, pool, database_dialect, database_role) class _Database(object): @@ -868,7 +876,9 @@ def __init__( database_id="database_id", pool=None, database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, + database_role=None, ): self.name = database_id self.pool = pool self.database_dialect = database_dialect + self.database_role = database_role From a107f1fdd4852b3692ae2ac1a0ec91a93c889e03 Mon Sep 17 00:00:00 2001 From: Walt Askew Date: Wed, 23 Apr 2025 16:36:49 +0000 Subject: [PATCH 3/3] add missing newline to code block --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 982d01c780..085587e51d 100644 --- a/README.rst +++ b/README.rst @@ -255,6 +255,7 @@ Connection API represents a wrap-around for Python Spanner API, written in accor If using [fine-grained access controls](https://cloud.google.com/spanner/docs/access-with-fgac) you can pass a ``database_role`` argument to connect as that role: .. code:: python + connection = connect("instance-id", "database-id", database_role='your-role')