Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e1a14e0
Implement instance level predefined roles
shayancanonical May 6, 2025
23c1ea0
Merge branch '16/edge' into feature/16_predefined_instance_roles
shayancanonical May 6, 2025
a5c6442
Fix minor bug introduced while rebasing off of 16/edge
shayancanonical May 6, 2025
09c4a71
Add integration test for charmed_read and charmed_dml roles
shayancanonical May 6, 2025
d778bba
Revert all major changes except introduction of predefined roles
shayancanonical May 7, 2025
873132e
Sweep diff and minor bug fixes
shayancanonical May 7, 2025
0dc8d3d
Avoid creating set_user extension
shayancanonical May 7, 2025
1764971
Merge branch '16/edge' into feature/16_predefined_instance_roles
shayancanonical May 7, 2025
fead5cd
Port Carl's fix for broken unit tests
shayancanonical May 7, 2025
2c0f822
Merge branch '16/edge' into feature/16_predefined_instance_roles
shayancanonical May 7, 2025
c848782
Merge branch '16/edge' into feature/16_predefined_instance_roles
shayancanonical May 28, 2025
8094e4f
Merge branch '16/edge' into feature/16_predefined_instance_roles
shayancanonical May 30, 2025
f1d3293
Remove admin and postgres roles
marceloneppel Jun 3, 2025
3aba882
Create DBA role
marceloneppel Jun 4, 2025
e16567d
Bump postgresql charm lib for 16/edge to v1 due to backwards incompat…
shayancanonical Jun 4, 2025
2c15212
Remove admin role test
marceloneppel Jun 4, 2025
f711479
Add DBA user test
marceloneppel Jun 4, 2025
1fed145
Test DBA role in replica
marceloneppel Jun 4, 2025
c8efa82
Grant reset_user function to DBA role
marceloneppel Jun 4, 2025
6cd2a85
Test set_user function for unprivileged users
marceloneppel Jun 4, 2025
fbebe06
Merge remote-tracking branch 'origin/feature/16_predefined_instance_r…
marceloneppel Jun 4, 2025
3ae3885
Merge remote-tracking branch 'origin/feature/16_predefined_dba_role' …
marceloneppel Jun 4, 2025
c78e2ff
Merge branch '16/edge' into feature/16_predefined_instance_roles
shayancanonical Jun 5, 2025
526bf2d
Re-add mistakenly removed patch statements
shayancanonical Jun 5, 2025
fd41021
Merge remote-tracking branch 'origin/feature/16_predefined_instance_r…
marceloneppel Jun 5, 2025
fa1efa7
Merge remote-tracking branch 'origin/feature/16_predefined_dba_role' …
marceloneppel Jun 5, 2025
f3eceba
Merge remote-tracking branch 'origin/16/edge' into feature/16_predefi…
marceloneppel Jun 5, 2025
9721e91
Merge remote-tracking branch 'origin/feature/16_predefined_dba_role' …
marceloneppel Jun 5, 2025
40c2051
Merge remote-tracking branch 'origin/16/edge' into feature/16_predefi…
marceloneppel Jun 6, 2025
d121c1f
Merge remote-tracking branch 'origin/feature/16_predefined_dba_role' …
marceloneppel Jun 6, 2025
dc5b01e
Reset connection to None before creating a new connection
marceloneppel Jun 6, 2025
262dc2b
Merge remote-tracking branch 'origin/feature/16_predefined_dba_role' …
marceloneppel Jun 6, 2025
24605d9
Merge remote-tracking branch 'origin/16/edge' into feature/16_predefi…
marceloneppel Jun 7, 2025
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
38 changes: 13 additions & 25 deletions lib/charms/postgresql_k8s/v1/postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@
ROLE_BACKUP = "charmed_backup"
ROLE_DBA = "charmed_dba"

# Groups to distinguish database permissions
PERMISSIONS_GROUP_ADMIN = "admin"

INVALID_EXTRA_USER_ROLE_BLOCKING_MESSAGE = "invalid role(s) for extra user roles"

REQUIRED_PLUGINS = {
Expand Down Expand Up @@ -246,7 +243,7 @@ def create_database(
Identifier(database)
)
)
for user_to_grant_access in [user, PERMISSIONS_GROUP_ADMIN, *self.system_users]:
for user_to_grant_access in [user, *self.system_users]:
cursor.execute(
SQL("GRANT ALL PRIVILEGES ON DATABASE {} TO {};").format(
Identifier(database), Identifier(user_to_grant_access)
Expand Down Expand Up @@ -291,20 +288,18 @@ def create_user(
"""
try:
# Separate roles and privileges from the provided extra user roles.
admin_role = False
roles = privileges = None
if extra_user_roles:
admin_role = PERMISSIONS_GROUP_ADMIN in extra_user_roles
valid_privileges, valid_roles = self.list_valid_privileges_and_roles()
roles = [
role
for role in extra_user_roles
if role in valid_roles and role != PERMISSIONS_GROUP_ADMIN
if role in valid_roles
]
privileges = {
extra_user_role
for extra_user_role in extra_user_roles
if extra_user_role not in roles and extra_user_role != PERMISSIONS_GROUP_ADMIN
if extra_user_role not in roles
}
invalid_privileges = [
privilege for privilege in privileges if privilege not in valid_privileges
Expand All @@ -321,8 +316,8 @@ def create_user(
if cursor.fetchone() is not None:
user_definition = "ALTER ROLE {}"
else:
user_definition = "CREATE ROLE {}"
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 ''}"
user_definition = "CREATE ROLE {} "
user_definition += f"WITH LOGIN{' SUPERUSER' if admin else ''} ENCRYPTED PASSWORD '{password}'"
if privileges:
user_definition += f" {' '.join(privileges)}"
cursor.execute(SQL("BEGIN;"))
Expand Down Expand Up @@ -804,22 +799,15 @@ def set_up_database(self, temp_location: Optional[str] = None) -> None:
cursor.execute(f"CREATE TABLESPACE temp LOCATION '{temp_location}';")
cursor.execute("GRANT CREATE ON TABLESPACE temp TO public;")

cursor.execute("SELECT TRUE FROM pg_roles WHERE rolname='admin';")
if cursor.fetchone() is None:
# Allow access to the postgres database only to the system users.
cursor.execute("REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;")
cursor.execute("REVOKE CREATE ON SCHEMA public FROM PUBLIC;")
for user in self.system_users:
cursor.execute(
SQL("GRANT ALL PRIVILEGES ON DATABASE postgres TO {};").format(
Identifier(user)
)
# Allow access to the postgres database only to the system users.
cursor.execute("REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;")
cursor.execute("REVOKE CREATE ON SCHEMA public FROM PUBLIC;")
for user in self.system_users:
cursor.execute(
SQL("GRANT ALL PRIVILEGES ON DATABASE postgres TO {};").format(
Identifier(user)
)
self.create_user(
PERMISSIONS_GROUP_ADMIN,
extra_user_roles=[ROLE_READ, ROLE_DML],
)
cursor.execute("GRANT CONNECT ON DATABASE postgres TO admin;")
except psycopg2.Error as e:
logger.error(f"Failed to set up databases: {e}")
raise PostgreSQLDatabasesSetupError() from e
Expand Down Expand Up @@ -907,7 +895,7 @@ def build_postgresql_group_map(group_map: Optional[str]) -> List[Tuple]:
ldap_group = mapping_parts[0]
psql_group = mapping_parts[1]

if psql_group in [*ACCESS_GROUPS, PERMISSIONS_GROUP_ADMIN]:
if psql_group in ACCESS_GROUPS:
logger.warning(f"Tried to assign LDAP users to forbidden group: {psql_group}")
continue

Expand Down
2 changes: 0 additions & 2 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,8 +1724,6 @@ def _start_primary(self, event: StartEvent) -> None: # noqa: C901
# This event can be run on a replica if the machines are restarted.
# For that case, check whether the postgres user already exits.
users = self.postgresql.list_users()
if "postgres" not in users:
self.postgresql.create_user("postgres", new_password(), admin=True)
# Create the backup user.
if BACKUP_USER not in users:
self.postgresql.create_user(
Expand Down
91 changes: 1 addition & 90 deletions tests/integration/new_relations/test_new_relations_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# See LICENSE file for licensing details.
import asyncio
import logging
import secrets
import string
from pathlib import Path

import psycopg2
Expand Down Expand Up @@ -484,97 +482,10 @@ async def test_relation_with_no_database_name(ops_test: OpsTest):
await ops_test.model.wait_for_idle(apps=APP_NAMES, status="active", raise_on_blocked=True)


async def test_admin_role(ops_test: OpsTest):
"""Test that the admin role gives access to all the databases."""
all_app_names = [DATA_INTEGRATOR_APP_NAME]
all_app_names.extend(APP_NAMES)
async def test_invalid_extra_user_roles(ops_test: OpsTest):
async with ops_test.fast_forward():
await ops_test.model.deploy(DATA_INTEGRATOR_APP_NAME, base=CHARM_BASE)
await ops_test.model.wait_for_idle(apps=[DATA_INTEGRATOR_APP_NAME], status="blocked")
await ops_test.model.applications[DATA_INTEGRATOR_APP_NAME].set_config({
"database-name": DATA_INTEGRATOR_APP_NAME.replace("-", "_"),
"extra-user-roles": "admin",
})
await ops_test.model.wait_for_idle(apps=[DATA_INTEGRATOR_APP_NAME], status="blocked")
await ops_test.model.add_relation(DATA_INTEGRATOR_APP_NAME, DATABASE_APP_NAME)
await ops_test.model.wait_for_idle(apps=all_app_names, status="active")

# Check that the user can access all the databases.
for database in [
DATABASE_DEFAULT_NAME,
f"{APPLICATION_APP_NAME.replace('-', '_')}_database",
"another_application_database",
]:
logger.info(f"connecting to the following database: {database}")
connection_string = await build_connection_string(
ops_test, DATA_INTEGRATOR_APP_NAME, "postgresql", database=database
)
connection = None
should_fail = False
try:
with psycopg2.connect(connection_string) as connection, connection.cursor() as cursor:
# Check the version that the application received is the same on the
# database server.
cursor.execute("SELECT version();")
data = cursor.fetchone()[0].split(" ")[1]

# Get the version of the database and compare with the information that
# was retrieved directly from the database.
version = await get_application_relation_data(
ops_test, DATA_INTEGRATOR_APP_NAME, "postgresql", "version"
)
assert version == data

# Write some data (it should fail in the default database name).
random_name = (
f"test_{''.join(secrets.choice(string.ascii_lowercase) for _ in range(10))}"
)
should_fail = database == DATABASE_DEFAULT_NAME
cursor.execute(f"CREATE SCHEMA test; CREATE TABLE test.{random_name}(data TEXT);")
if should_fail:
assert False, (
f"failed to run a statement in the following database: {database}"
)
except psycopg2.errors.InsufficientPrivilege as e:
if not should_fail:
logger.exception(e)
assert False, (
f"failed to connect to or run a statement in the following database: {database}"
)
finally:
if connection is not None:
connection.close()

# Test the creation and deletion of databases.
connection_string = await build_connection_string(
ops_test, DATA_INTEGRATOR_APP_NAME, "postgresql", database=DATABASE_DEFAULT_NAME
)
connection = psycopg2.connect(connection_string)
connection.autocommit = True
cursor = connection.cursor()
random_name = f"test_{''.join(secrets.choice(string.ascii_lowercase) for _ in range(10))}"
cursor.execute(f"CREATE DATABASE {random_name};")
cursor.execute(f"DROP DATABASE {random_name};")
try:
cursor.execute(f"DROP DATABASE {DATABASE_DEFAULT_NAME};")
assert False, (
f"the admin extra user role was able to drop the `{DATABASE_DEFAULT_NAME}` system database"
)
except psycopg2.errors.InsufficientPrivilege:
# Ignore the error, as the admin extra user role mustn't be able to drop
# the "postgres" system database.
pass
finally:
connection.close()


async def test_invalid_extra_user_roles(ops_test: OpsTest):
async with ops_test.fast_forward():
# Remove the relation between the database and the first data integrator.
await ops_test.model.applications[DATABASE_APP_NAME].remove_relation(
DATABASE_APP_NAME, DATA_INTEGRATOR_APP_NAME
)
await ops_test.model.wait_for_idle(apps=APP_NAMES, status="active", raise_on_blocked=True)

another_data_integrator_app_name = f"another-{DATA_INTEGRATOR_APP_NAME}"
data_integrator_apps_names = [DATA_INTEGRATOR_APP_NAME, another_data_integrator_app_name]
Expand Down
34 changes: 0 additions & 34 deletions tests/integration/new_relations/test_relations_coherence.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,40 +82,6 @@ async def test_relations(ops_test: OpsTest, charm):
f"{DATABASE_APP_NAME}:database", f"{DATA_INTEGRATOR_APP_NAME}:postgresql"
)

# Re-relation with admin role with checking permission
await ops_test.model.applications[DATA_INTEGRATOR_APP_NAME].set_config({
"database-name": DATA_INTEGRATOR_APP_NAME.replace("-", "_"),
"extra-user-roles": "admin",
})
await ops_test.model.wait_for_idle(apps=[DATA_INTEGRATOR_APP_NAME], status="blocked")
await ops_test.model.add_relation(DATA_INTEGRATOR_APP_NAME, DATABASE_APP_NAME)
await ops_test.model.wait_for_idle(apps=APP_NAMES, status="active")

connection_string = await build_connection_string(
ops_test,
DATA_INTEGRATOR_APP_NAME,
"postgresql",
database=DATA_INTEGRATOR_APP_NAME.replace("-", "_"),
)
try:
connection = psycopg2.connect(connection_string)
connection.autocommit = True
cursor = connection.cursor()
random_name = (
f"test_{''.join(secrets.choice(string.ascii_lowercase) for _ in range(10))}"
)
cursor.execute(f"CREATE DATABASE {random_name};")
except psycopg2.errors.InsufficientPrivilege:
assert False, (
f"failed connect to {random_name} or run a statement in the following database"
)
finally:
connection.close()

await ops_test.model.applications[DATABASE_APP_NAME].remove_relation(
f"{DATABASE_APP_NAME}:database", f"{DATA_INTEGRATOR_APP_NAME}:postgresql"
)

# Re-relation again with user role and checking write data
await ops_test.model.applications[DATA_INTEGRATOR_APP_NAME].set_config({
"database-name": DATA_INTEGRATOR_APP_NAME.replace("-", "_"),
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ def test_on_start(harness):
# Then test the event of a correct cluster bootstrapping.
_restart_services_after_reboot.reset_mock()
harness.charm.on.start.emit()
assert _postgresql.create_user.call_count == 4 # Considering the previous failed call.
assert _postgresql.create_user.call_count == 3 # Considering the previous failed call.
_oversee_users.assert_called_once()
_enable_disable_extensions.assert_called_once()
_set_primary_status_message.assert_called_once()
Expand Down
Loading