Skip to content

Commit 1bb0358

Browse files
Fixed databases access (#198)
1 parent 654a317 commit 1bb0358

File tree

4 files changed

+62
-5
lines changed

4 files changed

+62
-5
lines changed

lib/charms/postgresql_k8s/v0/postgresql.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
Any charm using this library should import the `psycopg2` or `psycopg2-binary` dependency.
2020
"""
2121
import logging
22-
from typing import Set
22+
from typing import List, Set
2323

2424
import psycopg2
2525
from psycopg2 import sql
@@ -32,7 +32,7 @@
3232

3333
# Increment this PATCH version before using `charmcraft publish-lib` or reset
3434
# to 0 if you are raising the major API version
35-
LIBPATCH = 9
35+
LIBPATCH = 10
3636

3737

3838
logger = logging.getLogger(__name__)
@@ -46,6 +46,10 @@ class PostgreSQLCreateUserError(Exception):
4646
"""Exception raised when creating a user fails."""
4747

4848

49+
class PostgreSQLDatabasesSetupError(Exception):
50+
"""Exception raised when the databases setup fails."""
51+
52+
4953
class PostgreSQLDeleteUserError(Exception):
5054
"""Exception raised when deleting a user fails."""
5155

@@ -76,12 +80,14 @@ def __init__(
7680
user: str,
7781
password: str,
7882
database: str,
83+
system_users: List[str] = [],
7984
):
8085
self.primary_host = primary_host
8186
self.current_host = current_host
8287
self.user = user
8388
self.password = password
8489
self.database = database
90+
self.system_users = system_users
8591

8692
def _connect_to_database(
8793
self, database: str = None, connect_to_current_host: bool = False
@@ -119,10 +125,16 @@ def create_database(self, database: str, user: str) -> None:
119125
if cursor.fetchone() is None:
120126
cursor.execute(sql.SQL("CREATE DATABASE {};").format(sql.Identifier(database)))
121127
cursor.execute(
122-
sql.SQL("GRANT ALL PRIVILEGES ON DATABASE {} TO {};").format(
123-
sql.Identifier(database), sql.Identifier(user)
128+
sql.SQL("REVOKE ALL PRIVILEGES ON DATABASE {} FROM PUBLIC;").format(
129+
sql.Identifier(database)
124130
)
125131
)
132+
for user_to_grant_access in [user] + self.system_users:
133+
cursor.execute(
134+
sql.SQL("GRANT ALL PRIVILEGES ON DATABASE {} TO {};").format(
135+
sql.Identifier(database), sql.Identifier(user_to_grant_access)
136+
)
137+
)
126138
with self._connect_to_database(database=database) as conn:
127139
with conn.cursor() as curs:
128140
statements = []
@@ -331,6 +343,26 @@ def list_users(self) -> Set[str]:
331343
logger.error(f"Failed to list PostgreSQL database users: {e}")
332344
raise PostgreSQLListUsersError()
333345

346+
def set_up_database(self) -> None:
347+
"""Set up postgres database with the right permissions."""
348+
connection = None
349+
try:
350+
with self._connect_to_database() as connection, connection.cursor() as cursor:
351+
# Allow access to the postgres database only to the system users.
352+
cursor.execute("REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;")
353+
for user in self.system_users:
354+
cursor.execute(
355+
sql.SQL("GRANT ALL PRIVILEGES ON DATABASE postgres TO {};").format(
356+
sql.Identifier(user)
357+
)
358+
)
359+
except psycopg2.Error as e:
360+
logger.error(f"Failed to set up databases: {e}")
361+
raise PostgreSQLDatabasesSetupError()
362+
finally:
363+
if connection is not None:
364+
connection.close()
365+
334366
def update_user_password(self, username: str, password: str) -> None:
335367
"""Update a user password.
336368

src/charm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def postgresql(self) -> PostgreSQL:
193193
user=USER,
194194
password=self.get_secret("app", f"{USER}-password"),
195195
database="postgres",
196+
system_users=SYSTEM_USERS,
196197
)
197198

198199
@property
@@ -569,6 +570,8 @@ def _initialize_cluster(self, event: WorkloadEvent) -> bool:
569570
extra_user_roles="pg_monitor",
570571
)
571572

573+
self.postgresql.set_up_database()
574+
572575
# Mark the cluster as initialised.
573576
self._peers.data[self.app]["cluster_initialised"] = "True"
574577

tests/integration/new_relations/helpers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ async def build_connection_string(
1818
relation_id: str = None,
1919
relation_alias: str = None,
2020
read_only_endpoint: bool = False,
21+
database: str = None,
2122
) -> str:
2223
"""Build a PostgreSQL connection string.
2324
@@ -30,12 +31,14 @@ async def build_connection_string(
3031
to get connection data from
3132
read_only_endpoint: whether to choose the read-only endpoint
3233
instead of the read/write endpoint
34+
database: optional database to be used in the connection string
3335
3436
Returns:
3537
a PostgreSQL connection string
3638
"""
3739
# Get the connection data exposed to the application through the relation.
38-
database = f'{application_name.replace("-", "_")}_{relation_name.replace("-", "_")}'
40+
if database is None:
41+
database = f'{application_name.replace("-", "_")}_{relation_name.replace("-", "_")}'
3942
username = await get_application_relation_data(
4043
ops_test, application_name, relation_name, "username", relation_id, relation_alias
4144
)

tests/integration/new_relations/test_new_relations.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,25 @@ async def test_two_applications_doesnt_share_the_same_relation_data(
180180

181181
assert application_connection_string != another_application_connection_string
182182

183+
# Check that the user cannot access other databases.
184+
for application, other_application_database in [
185+
(APPLICATION_APP_NAME, "another_application_first_database"),
186+
(another_application_app_name, "application_first_database"),
187+
]:
188+
connection_string = await build_connection_string(
189+
ops_test, application, FIRST_DATABASE_RELATION_NAME, database="postgres"
190+
)
191+
with pytest.raises(psycopg2.Error):
192+
psycopg2.connect(connection_string)
193+
connection_string = await build_connection_string(
194+
ops_test,
195+
application,
196+
FIRST_DATABASE_RELATION_NAME,
197+
database=other_application_database,
198+
)
199+
with pytest.raises(psycopg2.Error):
200+
psycopg2.connect(connection_string)
201+
183202

184203
async def test_an_application_can_connect_to_multiple_database_clusters(
185204
ops_test: OpsTest, database_charm

0 commit comments

Comments
 (0)