Skip to content

Commit 343a236

Browse files
[DPE-2167] Added admin extra user role (#201)
* Added admin extra user role * Fixed admin role permissions and added integration test * Removed flag from poetry exportD * Fixed comment and removed restriction to superuser * Simplify admin role privileges
1 parent 4c792b2 commit 343a236

File tree

2 files changed

+103
-10
lines changed

2 files changed

+103
-10
lines changed

lib/charms/postgresql_k8s/v0/postgresql.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = 10
35+
LIBPATCH = 11
3636

3737

3838
logger = logging.getLogger(__name__)
@@ -129,7 +129,7 @@ def create_database(self, database: str, user: str) -> None:
129129
sql.Identifier(database)
130130
)
131131
)
132-
for user_to_grant_access in [user] + self.system_users:
132+
for user_to_grant_access in [user, "admin"] + self.system_users:
133133
cursor.execute(
134134
sql.SQL("GRANT ALL PRIVILEGES ON DATABASE {} TO {};").format(
135135
sql.Identifier(database), sql.Identifier(user_to_grant_access)
@@ -165,7 +165,7 @@ def create_database(self, database: str, user: str) -> None:
165165
raise PostgreSQLCreateDatabaseError()
166166

167167
def create_user(
168-
self, user: str, password: str, admin: bool = False, extra_user_roles: str = None
168+
self, user: str, password: str = None, admin: bool = False, extra_user_roles: str = None
169169
) -> None:
170170
"""Creates a database user.
171171
@@ -178,28 +178,28 @@ def create_user(
178178
try:
179179
with self._connect_to_database() as connection, connection.cursor() as cursor:
180180
# Separate roles and privileges from the provided extra user roles.
181+
admin_role = False
181182
roles = privileges = None
182183
if extra_user_roles:
183184
extra_user_roles = tuple(extra_user_roles.lower().split(","))
184185
cursor.execute(
185186
"SELECT rolname FROM pg_roles WHERE rolname IN %s;", (extra_user_roles,)
186187
)
187-
roles = [role[0] for role in cursor.fetchall()]
188-
privileges = [
188+
admin_role = "admin" in extra_user_roles
189+
roles = [role[0] for role in cursor.fetchall() if role[0] != "admin"]
190+
privileges = {
189191
extra_user_role
190192
for extra_user_role in extra_user_roles
191-
if extra_user_role not in roles
192-
]
193+
if extra_user_role not in roles and extra_user_role != "admin"
194+
}
193195

194196
# Create or update the user.
195197
cursor.execute(f"SELECT TRUE FROM pg_roles WHERE rolname='{user}';")
196198
if cursor.fetchone() is not None:
197199
user_definition = "ALTER ROLE {}"
198200
else:
199201
user_definition = "CREATE ROLE {}"
200-
user_definition += (
201-
f"WITH LOGIN{' SUPERUSER' if admin else ''} ENCRYPTED PASSWORD '{password}'"
202-
)
202+
user_definition += f"WITH {'NOLOGIN' if user == 'admin' else 'LOGIN'}{' SUPERUSER' if admin else ''} ENCRYPTED PASSWORD '{password}'{'IN ROLE admin CREATEDB' if admin_role else ''}"
203203
if privileges:
204204
user_definition += f' {" ".join(privileges)}'
205205
cursor.execute(sql.SQL(f"{user_definition};").format(sql.Identifier(user)))
@@ -347,15 +347,21 @@ def set_up_database(self) -> None:
347347
"""Set up postgres database with the right permissions."""
348348
connection = None
349349
try:
350+
self.create_user(
351+
"admin",
352+
extra_user_roles="pg_read_all_data,pg_write_all_data",
353+
)
350354
with self._connect_to_database() as connection, connection.cursor() as cursor:
351355
# Allow access to the postgres database only to the system users.
352356
cursor.execute("REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;")
357+
cursor.execute("REVOKE CREATE ON SCHEMA public FROM PUBLIC;")
353358
for user in self.system_users:
354359
cursor.execute(
355360
sql.SQL("GRANT ALL PRIVILEGES ON DATABASE postgres TO {};").format(
356361
sql.Identifier(user)
357362
)
358363
)
364+
cursor.execute("GRANT CONNECT ON DATABASE postgres TO admin;")
359365
except psycopg2.Error as e:
360366
logger.error(f"Failed to set up databases: {e}")
361367
raise PostgreSQLDatabasesSetupError()

tests/integration/new_relations/test_new_relations.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# See LICENSE file for licensing details.
44
import asyncio
55
import logging
6+
import secrets
7+
import string
68
from pathlib import Path
79

810
import psycopg2
@@ -27,6 +29,7 @@
2729
APPLICATION_APP_NAME = "application"
2830
DATABASE_APP_NAME = "database"
2931
ANOTHER_DATABASE_APP_NAME = "another-database"
32+
DATA_INTEGRATOR_APP_NAME = "data-integrator"
3033
APP_NAMES = [APPLICATION_APP_NAME, DATABASE_APP_NAME, ANOTHER_DATABASE_APP_NAME]
3134
DATABASE_APP_METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
3235
FIRST_DATABASE_RELATION_NAME = "first-database"
@@ -397,3 +400,87 @@ async def test_relation_with_no_database_name(ops_test: OpsTest):
397400
f"{DATABASE_APP_NAME}", f"{APPLICATION_APP_NAME}:{NO_DATABASE_RELATION_NAME}"
398401
)
399402
await ops_test.model.wait_for_idle(apps=APP_NAMES, status="active", raise_on_blocked=True)
403+
404+
405+
async def test_admin_role(ops_test: OpsTest):
406+
"""Test that the admin role gives access to all the databases."""
407+
all_app_names = [DATA_INTEGRATOR_APP_NAME]
408+
all_app_names.extend(APP_NAMES)
409+
async with ops_test.fast_forward():
410+
await ops_test.model.deploy(DATA_INTEGRATOR_APP_NAME)
411+
await ops_test.model.wait_for_idle(apps=[DATA_INTEGRATOR_APP_NAME], status="blocked")
412+
await ops_test.model.applications[DATA_INTEGRATOR_APP_NAME].set_config(
413+
{
414+
"database-name": DATA_INTEGRATOR_APP_NAME.replace("-", "_"),
415+
"extra-user-roles": "admin",
416+
}
417+
)
418+
await ops_test.model.wait_for_idle(apps=[DATA_INTEGRATOR_APP_NAME], status="blocked")
419+
await ops_test.model.add_relation(DATA_INTEGRATOR_APP_NAME, DATABASE_APP_NAME)
420+
await ops_test.model.wait_for_idle(apps=all_app_names, status="active")
421+
422+
# Check that the user can access all the databases.
423+
for database in [
424+
"postgres",
425+
"application_first_database",
426+
"another_application_first_database",
427+
]:
428+
logger.info(f"connecting to the following database: {database}")
429+
connection_string = await build_connection_string(
430+
ops_test, DATA_INTEGRATOR_APP_NAME, "postgresql", database=database
431+
)
432+
connection = None
433+
should_fail = False
434+
try:
435+
with psycopg2.connect(connection_string) as connection, connection.cursor() as cursor:
436+
# Check the version that the application received is the same on the
437+
# database server.
438+
cursor.execute("SELECT version();")
439+
data = cursor.fetchone()[0].split(" ")[1]
440+
441+
# Get the version of the database and compare with the information that
442+
# was retrieved directly from the database.
443+
version = await get_application_relation_data(
444+
ops_test, DATA_INTEGRATOR_APP_NAME, "postgresql", "version"
445+
)
446+
assert version == data
447+
448+
# Write some data (it should fail in the "postgres" database).
449+
random_name = (
450+
f"test_{''.join(secrets.choice(string.ascii_lowercase) for _ in range(10))}"
451+
)
452+
should_fail = database == "postgres"
453+
cursor.execute(f"CREATE TABLE {random_name}(data TEXT);")
454+
if should_fail:
455+
assert (
456+
False
457+
), f"failed to run a statement in the following database: {database}"
458+
except psycopg2.errors.InsufficientPrivilege as e:
459+
if not should_fail:
460+
logger.exception(e)
461+
assert (
462+
False
463+
), f"failed to connect to or run a statement in the following database: {database}"
464+
finally:
465+
if connection is not None:
466+
connection.close()
467+
468+
# Test the creation and deletion of databases.
469+
connection_string = await build_connection_string(
470+
ops_test, DATA_INTEGRATOR_APP_NAME, "postgresql", database="postgres"
471+
)
472+
connection = psycopg2.connect(connection_string)
473+
connection.autocommit = True
474+
cursor = connection.cursor()
475+
random_name = f"test_{''.join(secrets.choice(string.ascii_lowercase) for _ in range(10))}"
476+
cursor.execute(f"CREATE DATABASE {random_name};")
477+
cursor.execute(f"DROP DATABASE {random_name};")
478+
try:
479+
cursor.execute("DROP DATABASE postgres;")
480+
assert False, "the admin extra user role was able to drop the `postgres` system database"
481+
except psycopg2.errors.InsufficientPrivilege:
482+
# Ignore the error, as the admin extra user role mustn't be able to drop
483+
# the "postgres" system database.
484+
pass
485+
finally:
486+
connection.close()

0 commit comments

Comments
 (0)