Skip to content

Commit 4088969

Browse files
committed
feat: support PG Notify for event streams using credentials
Create a Postgres credential that can store certificates and keys or userid/password at server startup and attach it to Activations that use EventStream. This allows us to test mTLS for Postgres, a sample pg_hba file is attached to this PR. To test this you need to create certificates and keys for * Postgres Server called (server.crt and server.key) * EDA Server called (client.crt and client.key) * You also need to create the CA certificate These files have to be present in tools/docker/postgres_ssl_config/certs The docker-compose file tools/docker/docker-compose-mac-pg-mtls.yml can be used to test mTLS The docker-compose file tools/docker/docker-compose-mac.yml can be used to test userid/password auth
1 parent 5c3755a commit 4088969

File tree

9 files changed

+611
-26
lines changed

9 files changed

+611
-26
lines changed

src/aap_eda/api/serializers/activation.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,24 +66,25 @@
6666
"rulebook_hash",
6767
]
6868

69+
PG_NOTIFY_DSN = (
70+
"host={{postgres_db_host}} port={{postgres_db_port}} "
71+
"dbname={{postgres_db_name}} user={{postgres_db_user}} "
72+
"password={{postgres_db_password}} sslmode={{postgres_sslmode}} "
73+
"sslcert={{eda.filename.postgres_sslcert|default(None)}} "
74+
"sslkey={{eda.filename.postgres_sslkey|default(None)}} "
75+
"sslpassword={{postgres_sslpassword|default(None)}} "
76+
"sslrootcert={{eda.filename.postgres_sslrootcert|default(None)}}"
77+
)
78+
6979

7080
@dataclass
7181
class VaultData:
7282
password: str = secrets.token_urlsafe()
7383
password_used: bool = False
7484

7585

76-
def _update_event_stream_source(
77-
validated_data: dict, vault_data: VaultData
78-
) -> str:
86+
def _update_event_stream_source(validated_data: dict) -> str:
7987
try:
80-
vault_data.password_used = True
81-
encrypted_dsn = encrypt_string(
82-
password=vault_data.password,
83-
plaintext=settings.PG_NOTIFY_DSN,
84-
vault_id=EDA_SERVER_VAULT_LABEL,
85-
)
86-
8788
source_mappings = yaml.safe_load(validated_data["source_mappings"])
8889
sources_info = {}
8990
for source_map in source_mappings:
@@ -92,7 +93,7 @@ def _update_event_stream_source(
9293

9394
sources_info[obj.name] = {
9495
"ansible.eda.pg_listener": {
95-
"dsn": encrypted_dsn,
96+
"dsn": PG_NOTIFY_DSN,
9697
"channels": [obj.channel_name],
9798
},
9899
}
@@ -334,7 +335,7 @@ def to_representation(self, activation):
334335
)
335336
eda_credentials = [
336337
EdaCredentialSerializer(credential).data
337-
for credential in activation.eda_credentials.all()
338+
for credential in activation.eda_credentials.filter(managed=False)
338339
]
339340
extra_var = (
340341
replace_vault_data(activation.extra_var)
@@ -476,8 +477,14 @@ def create(self, validated_data):
476477

477478
if validated_data.get("source_mappings", []):
478479
validated_data["rulebook_rulesets"] = _update_event_stream_source(
479-
validated_data, vault_data
480+
validated_data
480481
)
482+
eda_credentials = validated_data.get("eda_credentials", [])
483+
postgres_cred = models.EdaCredential.objects.filter(
484+
name=settings.DEFAULT_SYSTEM_PG_NOTIFY_CREDENTIAL_NAME
485+
).first()
486+
eda_credentials.append(postgres_cred.id)
487+
validated_data["eda_credentials"] = eda_credentials
481488

482489
vault = _get_vault_credential_type()
483490

@@ -651,7 +658,7 @@ def to_representation(self, activation):
651658
)
652659
eda_credentials = [
653660
EdaCredentialSerializer(credential).data
654-
for credential in activation.eda_credentials.all()
661+
for credential in activation.eda_credentials.filter(managed=False)
655662
]
656663
extra_var = (
657664
replace_vault_data(activation.extra_var)

src/aap_eda/core/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class DefaultCredentialType(DjangoStrEnum):
9898
SOURCE_CONTROL = "Source Control"
9999
AAP = "Red Hat Ansible Automation Platform"
100100
GPG = "GPG Public Key"
101+
POSTGRES = "Postgres"
101102

102103

103104
# TODO: rename to "RulebookProcessStatus" or "ParentProcessStatus"

src/aap_eda/core/management/commands/create_initial_data.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
# limitations under the License.
1414
import hashlib
1515
import logging
16+
import os
1617
from urllib.parse import urlparse
1718

1819
from ansible_base.rbac import permission_registry
1920
from ansible_base.rbac.models import DABPermission, RoleDefinition
21+
from django.conf import settings
2022
from django.contrib.contenttypes.models import ContentType
2123
from django.core.exceptions import ImproperlyConfigured
2224
from django.core.management import BaseCommand
@@ -841,6 +843,97 @@
841843
"required": ["auth_type", "username", "password", "http_header_key"],
842844
}
843845

846+
POSTGRES_CREDENTIAL_INPUTS = {
847+
"fields": [
848+
{
849+
"id": "postgres_db_host",
850+
"label": "Postgres DB Host",
851+
"help_text": "Postgres DB Server",
852+
},
853+
{
854+
"id": "postgres_db_port",
855+
"label": "Postgres DB Port",
856+
"help_text": "Postgres DB Port",
857+
"default": "5432",
858+
},
859+
{
860+
"id": "postgres_db_name",
861+
"label": "Postgres DB Name",
862+
"help_text": "Postgres Database name",
863+
},
864+
{
865+
"id": "postgres_db_user",
866+
"label": "Postgres DB User",
867+
"help_text": "Postgres Database user",
868+
},
869+
{
870+
"id": "postgres_db_password",
871+
"label": "Postgres DB Password",
872+
"help_text": "Postgres Database password",
873+
"secret": True,
874+
},
875+
{
876+
"id": "postgres_sslmode",
877+
"label": "Postgres SSL Mode",
878+
"help_text": "Postgres SSL Mode",
879+
"choices": [
880+
"disable",
881+
"allow",
882+
"prefer",
883+
"require",
884+
"verify-ca",
885+
"verify-full",
886+
],
887+
"default": "prefer",
888+
},
889+
{
890+
"id": "postgres_sslcert",
891+
"label": "Postgres SSL Certificate",
892+
"help_text": "Postgres SSL Certificate",
893+
"multiline": True,
894+
"default": "",
895+
},
896+
{
897+
"id": "postgres_sslkey",
898+
"label": "Postgres SSL Key",
899+
"help_text": "Postgres SSL Key",
900+
"multiline": True,
901+
"secret": True,
902+
"default": "",
903+
},
904+
{
905+
"id": "postgres_sslpassword",
906+
"label": "Postgres SSL Password",
907+
"help_text": "Postgres SSL Password for key",
908+
"secret": True,
909+
"default": "",
910+
},
911+
{
912+
"id": "postgres_sslrootcert",
913+
"label": "Postgres SSL Root Certificate",
914+
"help_text": "Postgres SSL Root Certificate",
915+
"multiline": True,
916+
"default": "",
917+
},
918+
]
919+
}
920+
921+
POSTGRES_CREDENTIAL_INJECTORS = {
922+
"extra_vars": {
923+
"postgres_db_host": "{{ postgres_db_host }}",
924+
"postgres_db_port": "{{ postgres_db_port }}",
925+
"postgres_db_name": "{{ postgres_db_name }}",
926+
"postgres_db_user": "{{ postgres_db_user }}",
927+
"postgres_db_password": "{{ postgres_db_password }}",
928+
"postgres_sslpassword": "{{ postgres_sslpassword | default(None) }}",
929+
"postgres_sslmode": "{{ postgres_sslmode }}",
930+
},
931+
"file": {
932+
"template.postgres_sslcert": "{{ postgres_sslcert }}",
933+
"template.postgres_sslrootcert": "{{ postgres_sslrootcert }}",
934+
"template.postgres_sslkey": "{{ postgres_sslkey }}",
935+
},
936+
}
844937
CREDENTIAL_TYPES = [
845938
{
846939
"name": enums.DefaultCredentialType.SOURCE_CONTROL,
@@ -1014,6 +1107,14 @@
10141107
"the Basic authentication."
10151108
),
10161109
},
1110+
{
1111+
"name": enums.DefaultCredentialType.POSTGRES,
1112+
"kind": "cloud",
1113+
"namespace": "postgres",
1114+
"inputs": POSTGRES_CREDENTIAL_INPUTS,
1115+
"injectors": POSTGRES_CREDENTIAL_INJECTORS,
1116+
"managed": True,
1117+
},
10171118
]
10181119

10191120

@@ -1046,6 +1147,7 @@ class Command(BaseCommand):
10461147
@transaction.atomic
10471148
def handle(self, *args, **options):
10481149
self._preload_credential_types()
1150+
self._update_postgres_credentials()
10491151
self._copy_registry_credentials()
10501152
self._copy_scm_credentials()
10511153
self._create_org_roles()
@@ -1167,6 +1269,58 @@ def _copy_scm_credentials(self):
11671269
"Control eda-credentials"
11681270
)
11691271

1272+
def _update_postgres_credentials(self):
1273+
cred_type = models.CredentialType.objects.get(
1274+
name=enums.DefaultCredentialType.POSTGRES
1275+
)
1276+
inputs = {
1277+
"postgres_db_host": settings.ACTIVATION_DB_HOST,
1278+
"postgres_db_port": settings.DATABASES["default"]["PORT"],
1279+
"postgres_db_name": settings.DATABASES["default"]["NAME"],
1280+
"postgres_db_user": settings.DATABASES["default"]["USER"],
1281+
"postgres_db_password": settings.DATABASES["default"]["PASSWORD"],
1282+
"postgres_sslmode": settings.DATABASES["default"]["OPTIONS"][
1283+
"sslmode"
1284+
],
1285+
"postgres_sslcert": "",
1286+
"postgres_sslkey": "",
1287+
"postgres_sslrootcert": "",
1288+
}
1289+
1290+
if settings.DATABASES["default"]["OPTIONS"]["sslcert"]:
1291+
inputs["postgres_sslcert"] = self._read_file(
1292+
settings.DATABASES["default"]["OPTIONS"]["sslcert"],
1293+
"PGSSLCERT",
1294+
)
1295+
1296+
if settings.DATABASES["default"]["OPTIONS"]["sslkey"]:
1297+
inputs["postgres_sslkey"] = self._read_file(
1298+
settings.DATABASES["default"]["OPTIONS"]["sslkey"], "PGSSLKEY"
1299+
)
1300+
1301+
if settings.DATABASES["default"]["OPTIONS"]["sslrootcert"]:
1302+
inputs["postgres_sslrootcert"] = self._read_file(
1303+
settings.DATABASES["default"]["OPTIONS"]["sslrootcert"],
1304+
"PGSSLROOTCERT",
1305+
)
1306+
1307+
models.EdaCredential.objects.update_or_create(
1308+
name=settings.DEFAULT_SYSTEM_PG_NOTIFY_CREDENTIAL_NAME,
1309+
defaults={
1310+
"description": "Default PG Notify Credentials",
1311+
"managed": True,
1312+
"credential_type": cred_type,
1313+
"inputs": inputs_to_store(inputs),
1314+
"organization": get_default_organization(),
1315+
},
1316+
)
1317+
1318+
def _read_file(self, name: str, key: str):
1319+
if not os.path.exists(name):
1320+
raise ImproperlyConfigured(f"Missing {key} file: {name}")
1321+
with open(name) as f:
1322+
return f.read()
1323+
11701324
def _create_org_roles(self):
11711325
org_ct = ContentType.objects.get(model="organization")
11721326
created = updated = 0

src/aap_eda/settings/default.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -781,15 +781,6 @@ def get_rulebook_process_log_level() -> RulebookProcessLogLevel:
781781
"ACTIVATION_DB_HOST", "host.containers.internal"
782782
)
783783

784-
_DEFAULT_PG_NOTIFY_DSN = (
785-
f"host={ACTIVATION_DB_HOST} "
786-
f"port={DATABASES['default']['PORT']} "
787-
f"dbname={DATABASES['default']['NAME']} "
788-
f"user={DATABASES['default']['USER']} "
789-
f"password={DATABASES['default']['PASSWORD']}"
790-
)
791-
792-
PG_NOTIFY_DSN = settings.get("PG_NOTIFY_DSN", _DEFAULT_PG_NOTIFY_DSN)
793784
PG_NOTIFY_TEMPLATE_RULEBOOK = settings.get("PG_NOTIFY_TEMPLATE_RULEBOOK", None)
794785

795786
SAFE_PLUGINS_FOR_PORT_FORWARD = settings.get(
@@ -806,7 +797,11 @@ def get_rulebook_process_log_level() -> RulebookProcessLogLevel:
806797
f"port={DATABASES['default']['PORT']} "
807798
f"dbname={DATABASES['default']['NAME']} "
808799
f"user={DATABASES['default']['USER']} "
809-
f"password={DATABASES['default']['PASSWORD']}"
800+
f"password={DATABASES['default']['PASSWORD']} "
801+
f"sslmode={settings.get('PGSSLMODE', default='prefer')} "
802+
f"sslcert={settings.get('PGSSLCERT', default='None')} "
803+
f"sslkey={settings.get('PGSSLKEY', default='None')} "
804+
f"sslrootcert={settings.get('PGSSLROOTCERT', default='None')}"
810805
)
811806
PG_NOTIFY_DSN_SERVER = settings.get(
812807
"PG_NOTIFY_DSN_SERVER", _DEFAULT_PG_NOTIFY_DSN_SERVER
@@ -823,3 +818,5 @@ def get_rulebook_process_log_level() -> RulebookProcessLogLevel:
823818
MAX_PG_NOTIFY_MESSAGE_SIZE = int(
824819
settings.get("MAX_PG_NOTIFY_MESSAGE_SIZE", 6144)
825820
)
821+
822+
DEFAULT_SYSTEM_PG_NOTIFY_CREDENTIAL_NAME = "_DEFAULT_EDA_PG_NOTIFY_CREDS"

tests/integration/api/test_activation_with_event_stream.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,11 @@ def create_activation(fks: dict):
282282

283283
@pytest.mark.django_db
284284
def test_create_activation_with_event_stream(
285-
admin_client: APIClient, preseed_credential_types
285+
admin_client: APIClient,
286+
preseed_credential_types,
287+
create_initial_data_command,
286288
):
289+
create_initial_data_command.handle()
287290
fks = create_activation_related_data(["demo"])
288291
test_activation = TEST_ACTIVATION.copy()
289292
test_activation["decision_environment_id"] = fks["decision_environment_id"]
@@ -674,12 +677,14 @@ def test_create_activation_with_duplicate_event_stream_name(
674677
def test_bad_src_activation_with_event_stream(
675678
admin_client: APIClient,
676679
preseed_credential_types,
680+
create_initial_data_command,
677681
source_tuples,
678682
rulesets,
679683
status_code,
680684
message,
681685
error_key,
682686
):
687+
create_initial_data_command.handle()
683688
names = [event_stream_name for _, event_stream_name in source_tuples]
684689
fks = create_activation_related_data(names, True, rulesets)
685690
test_activation = TEST_ACTIVATION.copy()

0 commit comments

Comments
 (0)