diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e907b9..67dd63f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: helm install postgres bitnami/postgresql \ --namespace postgres \ --version 18.5.14 \ - --set auth.postgresPassword=devpassword \ + -f postgres-dev-values.yaml \ --wait - name: Deploy operator diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5536657..90588cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,18 +9,23 @@ on: - src/** - Dockerfile +env: + CHART_FILE: charts/postgres-db-admin-operator/Chart.yaml + jobs: release: runs-on: ubuntu-latest permissions: packages: write + contents: write steps: - uses: actions/checkout@v4 - name: Get chart version id: chart run: | - echo "version=$(grep '^version:' charts/postgres-db-admin-operator/Chart.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT + echo "version=$(grep '^version:' ${{ env.CHART_FILE }} | awk '{print $2}')" >> $GITHUB_OUTPUT + echo "chart_changed=$( git diff --name-only HEAD~1 HEAD | grep -q '${{ env.CHART_FILE }}' && echo true || echo false)" >> $GITHUB_OUTPUT - name: Log in to GHCR uses: docker/login-action@v3 @@ -47,3 +52,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.actor }} registry_password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create GitHub release + if: steps.chart.outputs.chart_changed == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.chart.outputs.version }} + name: v${{ steps.chart.outputs.version }} + generate_release_notes: true diff --git a/Makefile b/Makefile index d546447..195031b 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ init: helm install $(POSTGRES_RELEASE) bitnami/postgresql \ --namespace $(POSTGRES_NAMESPACE) \ --version $(POSTGRES_CHART_VERSION) \ - --set auth.postgresPassword=$(POSTGRES_PASSWORD) \ + -f postgres-dev-values.yaml \ --wait deploy: @@ -27,6 +27,9 @@ deploy: --from-literal=password=$(POSTGRES_PASSWORD) \ --dry-run=client -o yaml | kubectl apply -f - helm upgrade --install postgres-db-admin-operator ./charts/postgres-db-admin-operator \ + --set image.repository=postgres-db-admin-operator \ + --set image.tag=dev \ + --set image.pullPolicy=Never \ --set postgres.host=$(POSTGRES_RELEASE)-postgresql.$(POSTGRES_NAMESPACE).svc.cluster.local \ --set postgres.user=postgres \ --set postgres.password.existingSecret=$(SECRET_NAME) diff --git a/charts/postgres-db-admin-operator/Chart.yaml b/charts/postgres-db-admin-operator/Chart.yaml index b4cfed5..d6c25fe 100644 --- a/charts/postgres-db-admin-operator/Chart.yaml +++ b/charts/postgres-db-admin-operator/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: postgres-db-admin-operator description: Kubernetes operator for managing PostgreSQL databases type: application -version: 0.0.6 -appVersion: 0.0.6 +version: 0.0.7 +appVersion: 0.0.7 diff --git a/postgres-dev-values.yaml b/postgres-dev-values.yaml new file mode 100644 index 0000000..bb7c561 --- /dev/null +++ b/postgres-dev-values.yaml @@ -0,0 +1,15 @@ +auth: + postgresPassword: devpassword + +architecture: standalone + +primary: + persistence: + enabled: false + livenessProbe: + initialDelaySeconds: 5 + pdb: + create: false + +global: + defaultFips: "off" diff --git a/scripts/integration_test.py b/scripts/integration_test.py index 959f3be..ef0fdea 100644 --- a/scripts/integration_test.py +++ b/scripts/integration_test.py @@ -7,12 +7,15 @@ - Postgres reachable via port-forward on localhost:5432 """ +import random +import string import time import psycopg from kubernetes import client, config from kubernetes.client.rest import ApiException -DB_NAME = "integration-test" +_SUFFIX = "".join(random.choices(string.ascii_lowercase, k=5)) +DB_NAME = f"integration-test-{_SUFFIX}" SECRET_NAME = f"{DB_NAME}-credentials" NAMESPACE = "default" PG_HOST = "localhost" diff --git a/tests/test_roles.py b/tests/test_roles.py index edf3929..4fdb093 100644 --- a/tests/test_roles.py +++ b/tests/test_roles.py @@ -1,8 +1,11 @@ import pytest import psycopg import psycopg.errors +from psycopg import sql from postgres_db_admin_operator.db import ( + create_database, + drop_database, create_admin_role, setup_admin_role_privileges, create_readonly_role, @@ -118,3 +121,75 @@ def test_drop_role(conn, role_db): def test_drop_role_idempotent(conn): drop_role(conn, "definitely-does-not-exist-role") + + +@pytest.fixture +def two_isolated_dbs(conn): + """Creates two fully-provisioned databases (admin + readonly roles each), cleans up on teardown.""" + db_a = "test-isolation-db-a" + db_b = "test-isolation-db-b" + + for db in [db_a, db_b]: + drop_database(conn, db) + create_database(conn, db) + create_admin_role(conn, db, ROLE_PASSWORD) + create_readonly_role(conn, db, ROLE_PASSWORD) + with psycopg.connect( + host="localhost", + port=POSTGRES_PORT, + user="postgres", + password=POSTGRES_PASSWORD, + dbname=db, + autocommit=True, + ) as db_conn: + setup_admin_role_privileges(db_conn, db) + setup_readonly_role_privileges(db_conn, db) + # Seed table created by postgres so default privilege grants apply to both roles + db_conn.execute("CREATE TABLE seed (id int)") + db_conn.execute("INSERT INTO seed VALUES (1)") + + yield db_a, db_b + + for db in [db_a, db_b]: + for role in [f"{db}_admin", f"{db}_readonly"]: + if conn.execute("SELECT 1 FROM pg_roles WHERE rolname = %s", (role,)).fetchone(): + with psycopg.connect( + host="localhost", + port=POSTGRES_PORT, + user="postgres", + password=POSTGRES_PASSWORD, + dbname=db, + autocommit=True, + ) as db_conn: + db_conn.execute(sql.SQL("DROP OWNED BY {}").format(sql.Identifier(role))) + conn.execute(sql.SQL("DROP ROLE {}").format(sql.Identifier(role))) + drop_database(conn, db) + + +def test_roles_cannot_access_other_database(two_isolated_dbs): + """Admin and readonly roles of one database must not have privileges in another database.""" + db_a, db_b = two_isolated_dbs + + for own_db, other_db in [(db_a, db_b), (db_b, db_a)]: + # Admin can insert into its own database + with connect_as(f"{own_db}_admin", own_db) as conn: + conn.execute("INSERT INTO seed VALUES (2)") + result = conn.execute("SELECT COUNT(*) FROM seed").fetchone() + assert result == (2,) + + # Readonly can select from its own database + with connect_as(f"{own_db}_readonly", own_db) as conn: + result = conn.execute("SELECT * FROM seed").fetchall() + assert result == [(1,), (2,)] + + # Admin cannot create a table in the other database + with connect_as(f"{own_db}_admin", other_db) as conn: + with pytest.raises(psycopg.errors.InsufficientPrivilege): + conn.execute("CREATE TABLE should_not_exist (id int)") + + # Readonly has no table grants in the other database + with connect_as(f"{own_db}_readonly", other_db) as conn: + result = conn.execute( + "SELECT COUNT(*) FROM information_schema.role_table_grants WHERE grantee = current_user" + ).fetchone() + assert result == (0,)