Skip to content

Commit 4a2dab2

Browse files
authored
Merge pull request #43 from ikalnytskyi/feat/ssl
Add SSL support
2 parents 50da8e8 + 0199df9 commit 4a2dab2

File tree

4 files changed

+77
-7
lines changed

4 files changed

+77
-7
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@ jobs:
4040

4141
- name: Run tests
4242
run: |
43-
python3 -m pip install --upgrade pip pytest psycopg furl
43+
python3 -m pip install --upgrade pip pytest psycopg furl cryptography
4444
python3 -m pytest -vv test_action.py
4545
env:
4646
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
4747
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
48+
CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }}
4849
EXPECTED_CONNECTION_URI: postgresql://postgres:postgres@localhost:5432/postgres
4950
EXPECTED_SERVICE_NAME: postgres
5051
EXPECTED_SERVER_VERSION: "16"
52+
EXPECTED_SSL: false
5153

5254
parametrized:
5355
runs-on: ${{ matrix.os }}
@@ -76,6 +78,7 @@ jobs:
7678
database: jedi_order
7779
port: 34837
7880
postgres-version: ${{ matrix.postgres-version }}
81+
ssl: true
7982
id: postgres
8083

8184
- name: Run setup-python
@@ -85,11 +88,13 @@ jobs:
8588

8689
- name: Run tests
8790
run: |
88-
python3 -m pip install --upgrade pip pytest psycopg furl
91+
python3 -m pip install --upgrade pip pytest psycopg furl cryptography
8992
python3 -m pytest -vv test_action.py
9093
env:
9194
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
9295
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
93-
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order
96+
CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }}
97+
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order?sslmode=verify-ca&sslrootcert=${{ steps.postgres.outputs.certificate-path }}
9498
EXPECTED_SERVICE_NAME: yoda
9599
EXPECTED_SERVER_VERSION: ${{ matrix.postgres-version }}
100+
EXPECTED_SSL: true

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ key features:
1010
* Runs on Linux, macOS and Windows action runners.
1111
* Adds PostgreSQL [client applications][1] to `PATH`.
1212
* PostgreSQL version can be parametrized.
13+
* Supports SSL if needed.
1314
* Easy [to verify][2] that it DOES NOT contain malicious code.
1415

1516
By default PostgreSQL 15 is used.
@@ -44,10 +45,11 @@ By default PostgreSQL 15 is used.
4445

4546
#### Outputs
4647

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

5254
#### User permissions
5355

@@ -74,6 +76,7 @@ steps:
7476
database: test
7577
port: 34837
7678
postgres-version: "14"
79+
ssl: "on"
7780
id: postgres
7881

7982
- run: pytest -vv tests/

action.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ inputs:
2424
postgres-version:
2525
description: The PostgreSQL major version to install. Either "14", "15", or "16".
2626
default: "16"
27+
ssl:
28+
description: When "true", encrypt connections using SSL (TLS).
29+
default: "false"
2730
required: false
2831
outputs:
2932
connection-uri:
@@ -32,6 +35,9 @@ outputs:
3235
service-name:
3336
description: The service name with connection parameters.
3437
value: ${{ steps.set-outputs.outputs.service-name }}
38+
certificate-path:
39+
description: The path to the server certificate if SSL is on.
40+
value: ${{ steps.set-outputs.outputs.certificate-path }}
3541
runs:
3642
using: composite
3743
steps:
@@ -132,6 +138,23 @@ runs:
132138
# directory we have no permissions to (owned by system postgres user).
133139
echo "unix_socket_directories = ''" >> "$PGDATA/postgresql.conf"
134140
echo "port = ${{ inputs.port }}" >> "$PGDATA/postgresql.conf"
141+
142+
if [ "${{ inputs.ssl }}" = "true" ]; then
143+
# On Windows, bash runs on top of MSYS2, which automatically converts
144+
# Unix paths to Windows paths for every argument that appears to be a
145+
# path. This behavior breaks the openssl invocation because the
146+
# subject argument is mistakenly converted when it should not be.
147+
# Therefore, we need to exclude it from the path conversion process
148+
# by setting the MSYS2_ARG_CONV_EXCL environment variable.
149+
#
150+
# https://www.msys2.org/docs/filesystem-paths/#automatic-unix-windows-path-conversion
151+
export MSYS2_ARG_CONV_EXCL="/CN"
152+
openssl req -new -x509 -days 365 -nodes -text -subj "/CN=localhost" \
153+
-out "$PGDATA/server.crt" -keyout "$PGDATA/server.key"
154+
chmod og-rwx "$PGDATA/server.key" "$PGDATA/server.crt"
155+
echo "ssl = on" >> "$PGDATA/postgresql.conf"
156+
fi
157+
135158
pg_ctl start --pgdata="$PGDATA"
136159
137160
# Save required connection parameters for created superuser to the
@@ -154,6 +177,12 @@ runs:
154177
password=${{ inputs.password }}
155178
dbname=${{ inputs.database }}
156179
EOF
180+
181+
if [ "${{ inputs.ssl }}" = "true" ]; then
182+
echo "sslmode=verify-ca" >> "$PGDATA/pg_service.conf"
183+
echo "sslrootcert=$PGDATA/server.crt" >> "$PGDATA/pg_service.conf"
184+
fi
185+
157186
echo "PGSERVICEFILE=$PGDATA/pg_service.conf" >> $GITHUB_ENV
158187
shell: bash
159188

@@ -173,6 +202,17 @@ runs:
173202
- name: Set action outputs
174203
run: |
175204
CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{ inputs.port }}/${{ inputs.database }}"
205+
CERTIFICATE_PATH="$RUNNER_TEMP/pgdata/server.crt"
206+
207+
if [ "${{ inputs.ssl }}" = "true" ]; then
208+
# Although SSLMODE and SSLROOTCERT are specific to libpq options,
209+
# most third-party drivers also support them. By default libpq
210+
# prefers SSL but doesn't require it, thus it's important to set
211+
# these options to ensure SSL is used and the certificate is
212+
# verified.
213+
CONNECTION_URI="$CONNECTION_URI?sslmode=verify-ca&sslrootcert=$CERTIFICATE_PATH"
214+
echo "certificate-path=$CERTIFICATE_PATH" >> $GITHUB_OUTPUT
215+
fi
176216
177217
echo "connection-uri=$CONNECTION_URI" >> $GITHUB_OUTPUT
178218
echo "service-name=${{ inputs.username }}" >> $GITHUB_OUTPUT

test_action.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import subprocess
55
import typing as t
66

7+
import cryptography.x509 as x509
78
import psycopg
89
import furl
910
import pytest
@@ -87,6 +88,20 @@ def test_service_name(service_name: str):
8788
assert service_name == os.getenv("EXPECTED_SERVICE_NAME")
8889

8990

91+
def test_certificate_path():
92+
"""Test that CERTIFICATE_PATH points to the certificate."""
93+
94+
certificate_path = os.getenv("CERTIFICATE_PATH")
95+
96+
if os.getenv("EXPECTED_SSL") == "true":
97+
assert certificate_path
98+
certificate_bytes = pathlib.Path(certificate_path).read_bytes()
99+
certificate = x509.load_pem_x509_certificate(certificate_bytes)
100+
assert certificate.subject.rfc4514_string() == "CN=localhost"
101+
else:
102+
assert not certificate_path
103+
104+
90105
def test_server_encoding(connection: psycopg.Connection):
91106
"""Test that PostgreSQL's encoding matches the one we passed to initdb."""
92107

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

149164

165+
def test_server_ssl(connection: psycopg.Connection):
166+
"""Test that connection is SSL encrypted."""
167+
168+
expected = os.getenv("EXPECTED_SSL") == "true"
169+
assert connection.info.pgconn.ssl_in_use is expected
170+
171+
150172
def test_user_permissions(connection: psycopg.Connection):
151173
"""Test that a user has super/createdb permissions."""
152174

0 commit comments

Comments
 (0)