Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSL support #43

Merged
merged 1 commit into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ jobs:

- name: Run tests
run: |
python3 -m pip install --upgrade pip pytest psycopg furl
python3 -m pip install --upgrade pip pytest psycopg furl cryptography
python3 -m pytest -vv test_action.py
env:
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }}
EXPECTED_CONNECTION_URI: postgresql://postgres:postgres@localhost:5432/postgres
EXPECTED_SERVICE_NAME: postgres
EXPECTED_SERVER_VERSION: "16"
EXPECTED_SSL: false

parametrized:
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -76,6 +78,7 @@ jobs:
database: jedi_order
port: 34837
postgres-version: ${{ matrix.postgres-version }}
ssl: true
id: postgres

- name: Run setup-python
Expand All @@ -85,11 +88,13 @@ jobs:

- name: Run tests
run: |
python3 -m pip install --upgrade pip pytest psycopg furl
python3 -m pip install --upgrade pip pytest psycopg furl cryptography
python3 -m pytest -vv test_action.py
env:
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order
CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }}
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order?sslmode=verify-ca&sslrootcert=${{ steps.postgres.outputs.certificate-path }}
EXPECTED_SERVICE_NAME: yoda
EXPECTED_SERVER_VERSION: ${{ matrix.postgres-version }}
EXPECTED_SSL: true
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ key features:
* Runs on Linux, macOS and Windows action runners.
* Adds PostgreSQL [client applications][1] to `PATH`.
* PostgreSQL version can be parametrized.
* Supports SSL if needed.
* Easy [to verify][2] that it DOES NOT contain malicious code.

By default PostgreSQL 15 is used.
Expand Down Expand Up @@ -44,10 +45,11 @@ By default PostgreSQL 15 is used.

#### Outputs

| Key | Description | Example |
|----------------|----------------------------------------------|-----------------------------------------------------|
| connection-uri | The connection URI to connect to PostgreSQL. | `postgresql://postgres:postgres@localhost/postgres` |
| service-name | The service name with connection parameters. | `postgres` |
| Key | Description | Example |
|------------------|--------------------------------------------------|-----------------------------------------------------|
| connection-uri | The connection URI to connect to PostgreSQL. | `postgresql://postgres:postgres@localhost/postgres` |
| service-name | The service name with connection parameters. | `postgres` |
| certificate-path | The path to the server certificate if SSL is on. | `/home/runner/work/_temp/pgdata/server.crt` |

#### User permissions

Expand All @@ -74,6 +76,7 @@ steps:
database: test
port: 34837
postgres-version: "14"
ssl: "on"
id: postgres

- run: pytest -vv tests/
Expand Down
40 changes: 40 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ inputs:
postgres-version:
description: The PostgreSQL major version to install. Either "14", "15", or "16".
default: "16"
ssl:
description: When "true", encrypt connections using SSL (TLS).
default: "false"
required: false
outputs:
connection-uri:
Expand All @@ -32,6 +35,9 @@ outputs:
service-name:
description: The service name with connection parameters.
value: ${{ steps.set-outputs.outputs.service-name }}
certificate-path:
description: The path to the server certificate if SSL is on.
value: ${{ steps.set-outputs.outputs.certificate-path }}
runs:
using: composite
steps:
Expand Down Expand Up @@ -132,6 +138,23 @@ runs:
# directory we have no permissions to (owned by system postgres user).
echo "unix_socket_directories = ''" >> "$PGDATA/postgresql.conf"
echo "port = ${{ inputs.port }}" >> "$PGDATA/postgresql.conf"

if [ "${{ inputs.ssl }}" = "true" ]; then
# On Windows, bash runs on top of MSYS2, which automatically converts
# Unix paths to Windows paths for every argument that appears to be a
# path. This behavior breaks the openssl invocation because the
# subject argument is mistakenly converted when it should not be.
# Therefore, we need to exclude it from the path conversion process
# by setting the MSYS2_ARG_CONV_EXCL environment variable.
#
# https://www.msys2.org/docs/filesystem-paths/#automatic-unix-windows-path-conversion
export MSYS2_ARG_CONV_EXCL="/CN"
openssl req -new -x509 -days 365 -nodes -text -subj "/CN=localhost" \
-out "$PGDATA/server.crt" -keyout "$PGDATA/server.key"
chmod og-rwx "$PGDATA/server.key" "$PGDATA/server.crt"
echo "ssl = on" >> "$PGDATA/postgresql.conf"
fi

pg_ctl start --pgdata="$PGDATA"

# Save required connection parameters for created superuser to the
Expand All @@ -154,6 +177,12 @@ runs:
password=${{ inputs.password }}
dbname=${{ inputs.database }}
EOF

if [ "${{ inputs.ssl }}" = "true" ]; then
echo "sslmode=verify-ca" >> "$PGDATA/pg_service.conf"
echo "sslrootcert=$PGDATA/server.crt" >> "$PGDATA/pg_service.conf"
fi

echo "PGSERVICEFILE=$PGDATA/pg_service.conf" >> $GITHUB_ENV
shell: bash

Expand All @@ -173,6 +202,17 @@ runs:
- name: Set action outputs
run: |
CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{ inputs.port }}/${{ inputs.database }}"
CERTIFICATE_PATH="$RUNNER_TEMP/pgdata/server.crt"

if [ "${{ inputs.ssl }}" = "true" ]; then
# Although SSLMODE and SSLROOTCERT are specific to libpq options,
# most third-party drivers also support them. By default libpq
# prefers SSL but doesn't require it, thus it's important to set
# these options to ensure SSL is used and the certificate is
# verified.
CONNECTION_URI="$CONNECTION_URI?sslmode=verify-ca&sslrootcert=$CERTIFICATE_PATH"
echo "certificate-path=$CERTIFICATE_PATH" >> $GITHUB_OUTPUT
fi

echo "connection-uri=$CONNECTION_URI" >> $GITHUB_OUTPUT
echo "service-name=${{ inputs.username }}" >> $GITHUB_OUTPUT
Expand Down
22 changes: 22 additions & 0 deletions test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import subprocess
import typing as t

import cryptography.x509 as x509
import psycopg
import furl
import pytest
Expand Down Expand Up @@ -87,6 +88,20 @@ def test_service_name(service_name: str):
assert service_name == os.getenv("EXPECTED_SERVICE_NAME")


def test_certificate_path():
"""Test that CERTIFICATE_PATH points to the certificate."""

certificate_path = os.getenv("CERTIFICATE_PATH")

if os.getenv("EXPECTED_SSL") == "true":
assert certificate_path
certificate_bytes = pathlib.Path(certificate_path).read_bytes()
certificate = x509.load_pem_x509_certificate(certificate_bytes)
assert certificate.subject.rfc4514_string() == "CN=localhost"
else:
assert not certificate_path


def test_server_encoding(connection: psycopg.Connection):
"""Test that PostgreSQL's encoding matches the one we passed to initdb."""

Expand Down Expand Up @@ -147,6 +162,13 @@ def test_server_version(connection: psycopg.Connection):
assert server_version.split(".")[0] == os.getenv("EXPECTED_SERVER_VERSION")


def test_server_ssl(connection: psycopg.Connection):
"""Test that connection is SSL encrypted."""

expected = os.getenv("EXPECTED_SSL") == "true"
assert connection.info.pgconn.ssl_in_use is expected


def test_user_permissions(connection: psycopg.Connection):
"""Test that a user has super/createdb permissions."""

Expand Down
Loading