From a2bdf1113974e57f71a1ecbc872b1eeaa1d05925 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 18 Mar 2026 16:00:07 +0100 Subject: [PATCH] Add DB constraints for binary identifier lengths --- ...d_binary_identifier_length_constraints.exs | 46 ++++++++++++++ .../binary_identifier_constraints_test.exs | 61 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 priv/repo/migrations/20260318145744_add_binary_identifier_length_constraints.exs create mode 100644 test/parrhesia/storage/adapters/postgres/binary_identifier_constraints_test.exs diff --git a/priv/repo/migrations/20260318145744_add_binary_identifier_length_constraints.exs b/priv/repo/migrations/20260318145744_add_binary_identifier_length_constraints.exs new file mode 100644 index 0000000..13073a5 --- /dev/null +++ b/priv/repo/migrations/20260318145744_add_binary_identifier_length_constraints.exs @@ -0,0 +1,46 @@ +defmodule Parrhesia.Repo.Migrations.AddBinaryIdentifierLengthConstraints do + use Ecto.Migration + + @constraints [ + {"event_ids", "event_ids_id_length_check", "octet_length(id) = 32"}, + {"events", "events_id_length_check", "octet_length(id) = 32"}, + {"events", "events_pubkey_length_check", "octet_length(pubkey) = 32"}, + {"events", "events_sig_length_check", "octet_length(sig) = 64"}, + {"event_tags", "event_tags_event_id_length_check", "octet_length(event_id) = 32"}, + {"replaceable_event_state", "replaceable_event_state_pubkey_length_check", + "octet_length(pubkey) = 32"}, + {"replaceable_event_state", "replaceable_event_state_event_id_length_check", + "octet_length(event_id) = 32"}, + {"addressable_event_state", "addressable_event_state_pubkey_length_check", + "octet_length(pubkey) = 32"}, + {"addressable_event_state", "addressable_event_state_event_id_length_check", + "octet_length(event_id) = 32"}, + {"banned_pubkeys", "banned_pubkeys_pubkey_length_check", "octet_length(pubkey) = 32"}, + {"allowed_pubkeys", "allowed_pubkeys_pubkey_length_check", "octet_length(pubkey) = 32"}, + {"banned_events", "banned_events_event_id_length_check", "octet_length(event_id) = 32"}, + {"group_memberships", "group_memberships_pubkey_length_check", "octet_length(pubkey) = 32"}, + {"group_roles", "group_roles_pubkey_length_check", "octet_length(pubkey) = 32"}, + {"management_audit_logs", "management_audit_logs_actor_pubkey_length_check", + "actor_pubkey IS NULL OR octet_length(actor_pubkey) = 32"}, + {"acl_rules", "acl_rules_principal_length_check", "octet_length(principal) = 32"} + ] + + def up do + Enum.each(@constraints, fn {table_name, constraint_name, expression} -> + execute(""" + ALTER TABLE #{table_name} + ADD CONSTRAINT #{constraint_name} + CHECK (#{expression}) + """) + end) + end + + def down do + Enum.each(@constraints, fn {table_name, constraint_name, _expression} -> + execute(""" + ALTER TABLE #{table_name} + DROP CONSTRAINT #{constraint_name} + """) + end) + end +end diff --git a/test/parrhesia/storage/adapters/postgres/binary_identifier_constraints_test.exs b/test/parrhesia/storage/adapters/postgres/binary_identifier_constraints_test.exs new file mode 100644 index 0000000..b12ba84 --- /dev/null +++ b/test/parrhesia/storage/adapters/postgres/binary_identifier_constraints_test.exs @@ -0,0 +1,61 @@ +defmodule Parrhesia.Storage.Adapters.Postgres.BinaryIdentifierConstraintsTest do + use Parrhesia.IntegrationCase, async: false, sandbox: true + + alias Parrhesia.Repo + + test "events rejects malformed binary identifier lengths at the database layer" do + assert_check_violation( + """ + INSERT INTO events (created_at, id, pubkey, kind, content, sig, inserted_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) + """, + [ + 1_700_123_456, + :binary.copy(<<0x10>>, 31), + :binary.copy(<<0x20>>, 32), + 1, + "invalid event id length", + :binary.copy(<<0x30>>, 64) + ], + "events_id_length_check" + ) + end + + test "management audit logs allow nil actor pubkeys but reject malformed ones" do + assert {:ok, %{num_rows: 1}} = + Repo.query( + """ + INSERT INTO management_audit_logs (actor_pubkey, method, params, inserted_at) + VALUES ($1, $2, $3, NOW()) + """, + [nil, "ping", %{}] + ) + + assert_check_violation( + """ + INSERT INTO management_audit_logs (actor_pubkey, method, params, inserted_at) + VALUES ($1, $2, $3, NOW()) + """, + [:binary.copy(<<0x40>>, 31), "ping", %{}], + "management_audit_logs_actor_pubkey_length_check" + ) + end + + test "acl rules reject malformed principal lengths at the database layer" do + assert_check_violation( + """ + INSERT INTO acl_rules (principal_type, principal, capability, match, inserted_at) + VALUES ($1, $2, $3, $4, NOW()) + """, + ["pubkey", :binary.copy(<<0x50>>, 31), "sync_read", %{}], + "acl_rules_principal_length_check" + ) + end + + defp assert_check_violation(sql, params, constraint_name) do + assert {:error, + %Postgrex.Error{ + postgres: %{code: :check_violation, constraint: ^constraint_name} + }} = Repo.query(sql, params) + end +end