From 187c9e33c196f7286ec6ce7cf9bce298cb9a80a3 Mon Sep 17 00:00:00 2001 From: Marc Planelles Date: Mon, 19 May 2025 13:03:23 +0200 Subject: [PATCH] fix: enforce tenant name rules at creation Closes #549 --- lib/multitenancy.ex | 1 + .../20250519103535.json | 29 +++++++++++++ .../20250519103535_migrate_resources53.exs | 19 +++++++++ test/multitenancy_test.exs | 31 +++++++++++++- test/support/multitenancy/domain.ex | 1 + .../multitenancy/resources/named_org.ex | 41 +++++++++++++++++++ 6 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 priv/resource_snapshots/test_repo/multitenant_named_orgs/20250519103535.json create mode 100644 priv/test_repo/migrations/20250519103535_migrate_resources53.exs create mode 100644 test/support/multitenancy/resources/named_org.ex diff --git a/lib/multitenancy.ex b/lib/multitenancy.ex index 20e67c0c..6927a960 100644 --- a/lib/multitenancy.ex +++ b/lib/multitenancy.ex @@ -5,6 +5,7 @@ defmodule AshPostgres.MultiTenancy do @tenant_name_regex ~r/^[a-zA-Z0-9_-]+$/ def create_tenant!(tenant_name, repo) do + validate_tenant_name!(tenant_name) Ecto.Adapters.SQL.query!(repo, "CREATE SCHEMA IF NOT EXISTS \"#{tenant_name}\"", []) migrate_tenant(tenant_name, repo) diff --git a/priv/resource_snapshots/test_repo/multitenant_named_orgs/20250519103535.json b/priv/resource_snapshots/test_repo/multitenant_named_orgs/20250519103535.json new file mode 100644 index 00000000..982b3edb --- /dev/null +++ b/priv/resource_snapshots/test_repo/multitenant_named_orgs/20250519103535.json @@ -0,0 +1,29 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "name", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "63124167427BA3C61197814348217EFC967CDAA398102552836E26BD93E198C8", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "multitenant_named_orgs" +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20250519103535_migrate_resources53.exs b/priv/test_repo/migrations/20250519103535_migrate_resources53.exs new file mode 100644 index 00000000..6059ae3e --- /dev/null +++ b/priv/test_repo/migrations/20250519103535_migrate_resources53.exs @@ -0,0 +1,19 @@ +defmodule AshPostgres.TestRepo.Migrations.MigrateResources53 do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + create table(:multitenant_named_orgs, primary_key: false) do + add(:name, :text, null: false, primary_key: true) + end + end + + def down do + drop(table(:multitenant_named_orgs)) + end +end diff --git a/test/multitenancy_test.exs b/test/multitenancy_test.exs index 73f9cb52..0dd99520 100644 --- a/test/multitenancy_test.exs +++ b/test/multitenancy_test.exs @@ -2,7 +2,7 @@ defmodule AshPostgres.Test.MultitenancyTest do use AshPostgres.RepoCase, async: false require Ash.Query - alias AshPostgres.MultitenancyTest.{CompositeKeyPost, Org, Post, User} + alias AshPostgres.MultitenancyTest.{CompositeKeyPost, NamedOrg, Org, Post, User} alias AshPostgres.Test.Post, as: GlobalPost setup do @@ -292,4 +292,33 @@ defmodule AshPostgres.Test.MultitenancyTest do |> Ash.create!() end end + + test "rejects characters other than alphanumericals, - and _ on tenant creation" do + assert_raise( + Ash.Error.Unknown, + ~r/Tenant name must match ~r\/\^\[a-zA-Z0-9_-]\+\$\/, got:/, + fn -> + NamedOrg + |> Ash.Changeset.for_create(:create, %{name: "🚫"}) + |> Ash.create!() + end + ) + end + + test "rejects characters other than alphanumericals, - and _ when renaming tenant" do + org = + NamedOrg + |> Ash.Changeset.for_create(:create, %{name: "toto"}) + |> Ash.create!() + + assert_raise( + Ash.Error.Unknown, + ~r/Tenant name must match ~r\/\^\[a-zA-Z0-9_-]\+\$\/, got:/, + fn -> + org + |> Ash.Changeset.for_update(:update, %{name: "🚫"}) + |> Ash.update!() + end + ) + end end diff --git a/test/support/multitenancy/domain.ex b/test/support/multitenancy/domain.ex index 85f078da..9ad6f881 100644 --- a/test/support/multitenancy/domain.ex +++ b/test/support/multitenancy/domain.ex @@ -4,6 +4,7 @@ defmodule AshPostgres.MultitenancyTest.Domain do resources do resource(AshPostgres.MultitenancyTest.Org) + resource(AshPostgres.MultitenancyTest.NamedOrg) resource(AshPostgres.MultitenancyTest.User) resource(AshPostgres.MultitenancyTest.Post) resource(AshPostgres.MultitenancyTest.PostLink) diff --git a/test/support/multitenancy/resources/named_org.ex b/test/support/multitenancy/resources/named_org.ex new file mode 100644 index 00000000..537ea337 --- /dev/null +++ b/test/support/multitenancy/resources/named_org.ex @@ -0,0 +1,41 @@ +defmodule AshPostgres.MultitenancyTest.NamedOrg do + @moduledoc false + use Ash.Resource, + domain: AshPostgres.MultitenancyTest.Domain, + data_layer: AshPostgres.DataLayer + + defimpl Ash.ToTenant do + def to_tenant(%{name: name}, resource) do + if Ash.Resource.Info.data_layer(resource) == AshPostgres.DataLayer && + Ash.Resource.Info.multitenancy_strategy(resource) == :context do + "org_#{name}" + else + name + end + end + end + + attributes do + attribute(:name, :string, + primary_key?: true, + allow_nil?: false, + public?: true, + writable?: true + ) + end + + actions do + default_accept(:*) + + defaults([:create, :read, :update, :destroy]) + end + + postgres do + table "multitenant_named_orgs" + repo(AshPostgres.TestRepo) + + manage_tenant do + template(["org_", :name]) + end + end +end