pow icon indicating copy to clipboard operation
pow copied to clipboard

Multitenancy with Pow guide confusing

Open joepstender opened this issue 4 years ago • 32 comments

I'm trying to use the "Multitenancy with Pow" guide to set up authentication in an app that uses Triplex. The app creates a db prefix for each "Customer", so they can create their own blogs (as example content). Each Customers content should be separated after signing in.

After following the Triplex part of the guide I get:

Pow.Config.ConfigError at GET /session/new
No `:user` configuration option found.

Does it need repo_opts in the config? I would want to use something like repo_opts: [prefix: :current_tenant]... Should I use Pow's user migration as a tenant_migration?

joepstender avatar Apr 13 '20 14:04 joepstender

Sounds like the Pow config is not set right? I would check the config in MyAppWeb.Pow.TriplexSessionPlug.call/2 has the right :otp_app setting.

Are the users under the tenants as well? Then the guide is how it should be set up. The :repo_opts will be set dynamically in the MyAppWeb.Pow.TriplexSessionPlug plug.

danschultzer avatar Apr 13 '20 16:04 danschultzer

I copied the example SessionPlug from the guide.

defmodule TenantzWeb.PowTriplexSessionPlug do
  def init(config), do: config

  def call(conn, config) do
    tenant = conn.assigns[:current_tenant] || conn.assigns[:raw_current_tenant]
    prefix = Triplex.to_prefix(tenant)
    config = Keyword.put(config, :repo_opts, prefix: prefix)

    Pow.Plug.Session.call(conn, config)
  end
end

Are the users under the tenants as well?

That's one of the scenario's I'm comparing. In that case should the user migration happen for each tenant?

joepstender avatar Apr 13 '20 18:04 joepstender

I copied the example SessionPlug from the guide.

Have you set it with the proper :otp_app config in the endpoint?

plug TenantzWeb.PowTriplexSessionPlug, otp_app: :my_app

That's one of the scenario's I'm comparing. In that case should the user migration happen for each tenant?

Yeah.

danschultzer avatar Apr 13 '20 18:04 danschultzer

Ah found the error, I made a typo in the :app_name in the endpoint config... Thanks! 👍

joepstender avatar Apr 13 '20 19:04 joepstender

Sorry to have to reopen. How would I get Pow.Plug.create_user to use the current_tenant?

joepstender avatar Apr 18 '20 19:04 joepstender

Once the TenantzWeb .PowTriplexSessionPlug has been triggered you don't need to do anything as the :repo_opts has been set for the config.

danschultzer avatar Apr 18 '20 19:04 danschultzer

Somehow it doesn't.

Postgrex.Error at POST /firstcustomer/signup
ERROR 42P01 (undefined_table) relation "users" does not exist

As you can see I use the tenant_id in the url, I created a custom registration controller etc. What is the setup you use in the guide? I tried the default Pow registration path first, should I add some kind of session param to the form? How does Pow know which tentant it should use?

joepstender avatar Apr 19 '20 08:04 joepstender

Check here: https://github.com/joepstender/testpow

joepstender avatar Apr 19 '20 08:04 joepstender

You should move the plug Triplex.ParamPlug to before plug TestpowWeb.PowTriplexSessionPlug. I've added a section to the guide: https://github.com/danschultzer/pow/pull/495

danschultzer avatar Apr 19 '20 15:04 danschultzer

Unfortunately that doesn't seem to work. Same error message. I propose I write a step-by-step guide that could be checked and perhaps when we get it working used for https://powauth.com ?

Postgrex.Error at POST /firstcustomer/signup
ERROR 42P01 (undefined_table) relation "users" does not exist
    query: INSERT INTO "users" ("email","password_hash","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id"

joepstender avatar Apr 21 '20 09:04 joepstender

Sorry for the delay, got occupied by a lot of work. Let me test this locally.

danschultzer avatar Apr 23 '20 03:04 danschultzer

Works for me, I've added the demo here: https://github.com/pow-auth/pow_demo/compare/master..multitenancy-triplex

danschultzer avatar Apr 23 '20 04:04 danschultzer

Also, looking at the test repo, seems like there's no triplex plug?

danschultzer avatar Apr 23 '20 04:04 danschultzer

Thanks for reopening. I added the triplex plug here. In your test you do:

  defp set_triplex_tenant(conn, tenant) do
    conn = %{conn | params: %{"subdomain" => tenant}}
    opts = Triplex.ParamPlug.init(param: :subdomain)

    Triplex.ParamPlug.call(conn, opts)
  end

Perhaps I misunderstand how Triplex params work? I use the slug for the tenant. In tenant.ex:

  @derive {Phoenix.Param, key: :slug}

Then I get the tenant from the db using the slug:

  def get_tenant!(slug) do
    Tenant
    |> Repo.get_by!(%{slug: slug})
  end

In the controller:

tenant = Testpow.Customers.get_tenant!(conn.params["tenant_id"])

def create(conn, %{"user" => user_params}, tenant) do
    conn
    |> Pow.Plug.create_user(user_params)
    |> case do
      {:ok, _user, conn} ->
        conn
        |> put_flash(:info, "Welcome!")
        |> redirect(to: Routes.tenant_path(conn, :show, tenant))

      {:error, changeset, conn} ->
        render(conn, "new.html", changeset: changeset, tenant: tenant)
    end
  end

joepstender avatar Apr 23 '20 13:04 joepstender

Yeah, it's the Triplex plug that's missing: https://hexdocs.pm/triplex/1.3.0/readme.html#fetching-the-tenant-with-plug

It's easiest if you pull the tenant before calling the TestpowWeb.PowTriplexSessionPlug plug, so in your case I would set it up like this rather than pulling it in the controller:

plug Triplex.ParamPlug,
  param: :tenant_id,
  tenant_handler: &Testpow.Customers.get_tenant!/1

plug TestpowWeb.PowTriplexSessionPlug, otp_app: :testpow

danschultzer avatar Apr 23 '20 16:04 danschultzer

Hello,

I have read though this thread and am also having difficulties with being able to get the tenant / subdomain to use for the session.

It really seems that I am really close to having this up and running, but setting the tenant to the subdomain doesn't seem to be working.

I one of my index files, invoke pry, and in the conn:

raw_current_tenant: nil repo_opts: [prefix: nil]

I am running on localhost, and setup a domain one.whatevertesting.com:4000

However, if I change the triplex_session_plug for the config like this:

config = Keyword.put(config, :repo_opts, prefix: "one")

In this case the repo_opts: [prefix: "one"] - as it should be

but raw_current_tenant is still nil

Even if that is set, it does not seem to propagate through other files. Do you still need to update the functions in the Context ie:

def list_clients do
  Repo.all(Client, prefix: Triplex.to_prefix("one"))
end

That is the only way that I can seem to get the listing from the tenant. If I run without the prefix code, is shows the public repository. (I converted a non-tenant app before adding the tenancy)

Arni

ps. Thanks for this I had POW up and running easily, and seem to be close to having this as well. Looking forward to getting over the (hopefully) last hurdle

arnimikelsons avatar Apr 27 '20 19:04 arnimikelsons

@arnimikelsons you might not be setting the Triplex plug before the MyAppWeb.PowTriplexSessionPlug plug call in your endpoint:

plug Triplex.SubdomainPlug,
  endpoint: MyApp.Endpoint

# ...

plug MyAppWeb.PowTriplexSessionPlug, otp_app: :my_app

danschultzer avatar Apr 27 '20 19:04 danschultzer

Thanks for the quick reply.

When I put that in, I get an error:

ERROR 42P01 (undefined_table) relation "one.whatevertesting.com.users" does not exist

(the tenant is "one")

I tried changing the instance name from "one" to "one.whatevertesting.com", but that didn't work as well. I guess would need to get in touch with the Triplex people to do that??? I have been playing with a tutorial on subdomains, that does manage to pull out just the first part of the URL at: https://blog.gazler.com/blog/2015/07/18/subdomains-with-phoenix/

arnimikelsons avatar Apr 27 '20 23:04 arnimikelsons

I cannot get Triplex working correctly when putting the (Param)Plug in endpoint.ex, when I put it in the router (like Triplex documentation suggests) the :tenant, and :raw_current_tenant become available from the param.

The Triplex.ParamPlug

plug Triplex.ParamPlug,
  param: :id,
  tenant_handler: &MyAppWeb.TenantHelper.tenant_handler/1

And the TenantHelper module, using Triplex.exists? to avoid errors on non-tenant pages. If I use that :current_tenant won't be loaded.

defmodule MyAppWeb.TenantHelper do
  alias MyApp.Customers

  def tenant_handler(id) do
    if Triplex.exists?(id) do
      customer = Customers.get_customer!(id)
      Triplex.to_prefix(customer)
    end
  end
end

Next step is to test with Pow. I'll report back :-)

joepstender avatar Apr 28 '20 10:04 joepstender

@arnimikelsons yeah, it's a confusing error.

The error suggests that you haven't created the tenant with Triplex.create("one.whatevertesting.com") or haven't moved the user migration from priv/YOUR_REPO/migrations to priv/YOUR_REPO/tenant_migrations.

However it really should just be pulling out one instead of the whole domain. Looking at the source code, it seems you need to set the whatevertesting.com domain in the url endpoint setting for it to pull the subdomain. Should look something like this:

config :my_app, MyAppWeb.Endpoint,
  url: [host: "whatevertesting.com"],
  # ...

@joepstender I realize that I'm actually not testing the controller in the demo branch I linked above. Let me add a proper controller test to see if it works. I suspect that query params might not have been fetched.

Edit: Added controller tests, and it works: https://github.com/pow-auth/pow_demo/commit/297d8558b0c31105b7c357bf64c109ee07eb36a4

danschultzer avatar Apr 29 '20 01:04 danschultzer

I figured out that error by adding the host. Actually, would be good to the docs when you are editing the config file, that you need to do that. ie. put in the host name if you are using subdomains. It is obvious now that I see it, but it took a while. Still not quite connecting everything....

I have raw_current_user set as a private variable to "one" (picking it up from the subdomain)

I presume it should set the variable tenant in TriplexSessionPlug, so that it can be used elsewhere.

But if I try to put in: def list_clients do Repo.all(Client, prefix: Triplex.to_prefix(tenant)) end I get lib/client_app/client_base.ex:21: undefined function tenant/0. Do i need a function in the context to grab that variable? I would have thought the plug makes it available

def list_clients do Repo.all(Client, prefix: Triplex.to_prefix("one") end

It does list the client in that tenant. If I remove the prefix, it lists the public repository (I had it there before migrating it)

Sorry if these questions are basic, just learning the Phoenix / Elixir.

arnimikelsons avatar Apr 29 '20 02:04 arnimikelsons

Actually, would be good to the docs when you are editing the config file, that you need to do that. ie. put in the host name if you are using subdomains.

I agree. I think this is something the Triplex docs should have rather than Pow.

I presume it should set the variable tenant in TriplexSessionPlug, so that it can be used elsewhere.

It should be set by one of the triplex plugs: https://hexdocs.pm/triplex/1.3.0/readme.html#fetching-the-tenant-with-plug

Then it would be available with :current_tenant assigns. The Pow triplex demo should show how: https://github.com/pow-auth/pow_demo/compare/master..multitenancy-triplex

It might be good to add some logic to ensure that the tenant has been loaded though. I'll update the multitenancy guide to make that clearer.

If you don't want to use the Triplex plugs then I guess you could change the TriplexSessionPlug to set the tenant.

The problem in your code is that you don't pass the tenant to the method, should look like this:

def list_clients(tenant) do
  Repo.all(Client, prefix: Triplex.to_prefix(tenant))
end

danschultzer avatar Apr 29 '20 17:04 danschultzer

Thanks. What do I put in the controller to pass the subdomain over?

I have:

def index(conn, _params) do clients = ClientBase.list_clients("one") render(conn, "index.html", clients: clients) end

But, struggling how to turn the "one" into a variable

arnimikelsons avatar Apr 30 '20 23:04 arnimikelsons

Actually, I got it:

def index(conn, _params) do clients = ClientBase.list_clients(conn.private[:subdomain]) render(conn, "index.html", clients: clients) end

arnimikelsons avatar May 01 '20 04:05 arnimikelsons

Looks good, but you can also assign the prefix in your custom Pow Triplex plug:

defmodule MyAppWeb.Pow.TriplexSessionPlug do
  def init(config), do: config

  def call(conn, config) do
    tenant = conn.assigns[:current_tenant] || conn.assigns[:raw_current_tenant]
    prefix = Triplex.to_prefix(tenant)
    config = Keyword.put(config, :repo_opts, [prefix: prefix])

     conn
     |> Plug.Conn.assign(:repo_prefix, prefix)
     |> Pow.Plug.Session.call(conn, config)
  end
end

And then you can pass it like this:

def index(conn, _params) do
  clients = ClientBase.list_clients(conn.assigns[:repo_prefix])
  render(conn, "index.html", clients: clients)
end

And have the list_clients method look like this:

def list_clients(prefix) do
  Repo.all(Client, prefix: prefix)
end

danschultzer avatar May 01 '20 05:05 danschultzer

Seems that this is not taking the triplex prefix. Where would you add that?

Another note, which is kind of weird. If you have <%= require IEx; IEx.pry %> in the template (index.html.eex, in this case), then you get an "protocol Phoenix.HTML.Safe not implemented" error. Remove the line and the error goes away.

arnimikelsons avatar May 01 '20 17:05 arnimikelsons

Running into another problem with Registration. On submission, I am getting

ERROR 23502 (not_null_violation) null value in column "account_id" violates not-null constraint

This was working before using multi-tenancy, so I suspect that the relation is not on the tenant. Specifically, would need to include it into the tenant when running create_new_account_for_user, and not sure how to do that.

I have got the ClientAppWeb.Pow.TriplexSessionPlug working, so that I am getting the following values:

prefix: "org_two", tenant: "two"

It is from this tutorial: https://fullstackphoenix.com/tutorials/multi-tenancy-and-authentication-with-pow

arnimikelsons avatar May 01 '20 20:05 arnimikelsons

That tutorial handles multitenancy in a very different way, keeping all tenant data within the same DB tables. If you go for that approach, then you should scrap Triplex.

danschultzer avatar May 01 '20 22:05 danschultzer

Sorry, the multi-tenancy is in the next tutorial - https://fullstackphoenix.com/tutorials/multi-tenancy-and-phoenix-part-2. I think it is good, and probably should just have all user fields in one table. Will take another shot at it.

arnimikelsons avatar May 01 '20 23:05 arnimikelsons

I have redone it, with your instructions. Only thing, I could not get the Triplix plug to work. Maybe because it is localhost domains.. This is what I use:

defmodule TestpowWeb.Plug.Subdomain do
  import Plug.Conn

  @doc false
  def init(default), do: default

  @doc false
  def call(conn, _router) do
    case get_subdomain(conn.host) do
      subdomain when byte_size(subdomain) > 0 ->
        conn
        |> put_private(:subdomain, subdomain)
        |> put_private(:current_raw_tenant, subdomain)

      _ ->
        conn
    end
  end

  defp get_subdomain(host) do
    root_host = TestpowWeb.Endpoint.config(:url)[:host]
    String.replace(host, ~r/.?#{root_host}/, "")
  end
end

And change the the tenant line in triplex_session_plug.ex to:

      tenant = conn.private[:subdomain]

Which seems to work great

arnimikelsons avatar May 02 '20 21:05 arnimikelsons