diff --git a/Makefile b/Makefile index 91a0f819..1172da0e 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ dev: iex --name node1@127.0.0.1 --cookie cookie -S mix run --no-halt dev.node2: - PROXY_PORT=7655 \ + PROXY_PORT_TRANSACTION=7655 \ PORT=4001 \ MIX_ENV=dev \ VAULT_ENC_KEY="aHD8DZRdk2emnkdktFZRh3E9RNg4aOY7" \ diff --git a/VERSION b/VERSION index 27792457..769ed6ae 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.13 +0.2.14 diff --git a/config/runtime.exs b/config/runtime.exs index 69fb2f70..41b34ec1 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -148,7 +148,9 @@ if config_env() != :test do jwt_claim_validators: System.get_env("JWT_CLAIM_VALIDATORS", "{}") |> Jason.decode!(), api_jwt_secret: System.get_env("API_JWT_SECRET"), metrics_jwt_secret: System.get_env("METRICS_JWT_SECRET"), - proxy_port: System.get_env("PROXY_PORT", "7654") |> String.to_integer(), + proxy_port_transaction: + System.get_env("PROXY_PORT_TRANSACTION", "6543") |> String.to_integer(), + proxy_port_session: System.get_env("PROXY_PORT_SESSION", "5432") |> String.to_integer(), prom_poll_rate: System.get_env("PROM_POLL_RATE", "15000") |> String.to_integer(), global_upstream_ca: upstream_ca, global_downstream_cert: downstream_cert, diff --git a/config/test.exs b/config/test.exs index a75e46e8..4eab1568 100644 --- a/config/test.exs +++ b/config/test.exs @@ -6,7 +6,7 @@ config :supavisor, api_jwt_secret: "dev", metrics_jwt_secret: "dev", jwt_claim_validators: %{}, - proxy_port: System.get_env("PROXY_PORT", "7654") |> String.to_integer(), + proxy_port_transaction: System.get_env("PROXY_PORT_TRANSACTION", "7654") |> String.to_integer(), secondary_proxy_port: 7655, secondary_http: 4003, prom_poll_rate: 500 diff --git a/deploy/service/service_vars.ini b/deploy/service/service_vars.ini index d64ed392..546defd2 100644 --- a/deploy/service/service_vars.ini +++ b/deploy/service/service_vars.ini @@ -6,7 +6,8 @@ API_JWT_SECRET="" METRICS_JWT_SECRET="" ADDR_TYPE="" # inet/inet6 PORT="" -PROXY_PORT="" +PROXY_PORT_TRANSACTION="" +PROXY_PORT_SESSION="" LOGS_ENGINE="" LOGFLARE_API_KEY="" LOGFLARE_SOURCE_ID="" diff --git a/lib/supavisor.ex b/lib/supavisor.ex index c03e81ce..50aa7cfe 100644 --- a/lib/supavisor.ex +++ b/lib/supavisor.ex @@ -7,11 +7,11 @@ defmodule Supavisor do @registry Supavisor.Registry.Tenants @type workers :: %{manager: pid, pool: pid} - @spec start(String.t(), String.t()) :: {:ok, pid} | {:error, any()} - def start(tenant, user_alias) do + @spec start(String.t(), String.t(), fun(), atom() | nil) :: {:ok, pid} | {:error, any()} + def start(tenant, user_alias, client_key, def_mode_type \\ nil) do case get_global_sup(tenant, user_alias) do nil -> - start_local_pool(tenant, user_alias) + start_local_pool(tenant, user_alias, client_key, def_mode_type) pid -> {:ok, pid} @@ -97,8 +97,9 @@ defmodule Supavisor do ## Internal functions - @spec start_local_pool(String.t(), String.t()) :: {:ok, pid} | {:error, any()} - defp start_local_pool(tenant, user_alias) do + @spec start_local_pool(String.t(), String.t(), term(), atom() | nil) :: + {:ok, pid} | {:error, any()} + defp start_local_pool(tenant, user_alias, auth_secrets, def_mode_type) do Logger.debug("Starting pool for #{inspect({tenant, user_alias})}") case Tenants.get_pool_config(tenant, user_alias) do @@ -129,7 +130,9 @@ defmodule Supavisor do ip_version: H.ip_version(ip_ver, db_host), upstream_ssl: tenant_record.upstream_ssl, upstream_verify: tenant_record.upstream_verify, - upstream_tls_ca: H.upstream_cert(tenant_record.upstream_tls_ca) + upstream_tls_ca: H.upstream_cert(tenant_record.upstream_tls_ca), + require_user: tenant_record.require_user, + secrets: auth_secrets } args = %{ @@ -137,7 +140,7 @@ defmodule Supavisor do user_alias: user_alias, auth: auth, pool_size: pool_size, - mode: mode, + mode: def_mode_type || mode, default_parameter_status: ps } diff --git a/lib/supavisor/application.ex b/lib/supavisor/application.ex index b7801dcc..362eca22 100644 --- a/lib/supavisor/application.ex +++ b/lib/supavisor/application.ex @@ -16,21 +16,26 @@ defmodule Supavisor.Application do {Supavisor.SignalHandler, []} ) - proxy_port = Application.get_env(:supavisor, :proxy_port) + proxy_ports = [ + {Application.get_env(:supavisor, :proxy_port_transaction), :transaction}, + {Application.get_env(:supavisor, :proxy_port_session), :session} + ] - :ranch.start_listener( - :pg_proxy, - :ranch_tcp, - %{ - max_connections: String.to_integer(System.get_env("MAX_CONNECTIONS") || "25000"), - num_acceptors: String.to_integer(System.get_env("NUM_ACCEPTORS") || "100"), - socket_opts: [port: proxy_port] - }, - Supavisor.ClientHandler, - [] - ) - |> then(&"Proxy started on port #{proxy_port}, result: #{inspect(&1)}") - |> Logger.warning() + for {port, mode} <- proxy_ports do + :ranch.start_listener( + :pg_proxy, + :ranch_tcp, + %{ + max_connections: String.to_integer(System.get_env("MAX_CONNECTIONS") || "25000"), + num_acceptors: String.to_integer(System.get_env("NUM_ACCEPTORS") || "100"), + socket_opts: [port: port] + }, + Supavisor.ClientHandler, + %{def_mode_type: mode} + ) + |> then(&"Proxy started #{mode} on port #{port}, result: #{inspect(&1)}") + |> Logger.warning() + end :syn.set_event_handler(Supavisor.SynHandler) :syn.add_node_to_scopes([:tenants]) @@ -57,13 +62,14 @@ defmodule Supavisor.Application do SupavisorWeb.Telemetry, # Start the PubSub system {Phoenix.PubSub, name: Supavisor.PubSub}, - # Start the Endpoint (http/https) - SupavisorWeb.Endpoint, { PartitionSupervisor, child_spec: DynamicSupervisor, strategy: :one_for_one, name: Supavisor.DynamicSupervisor }, - Supavisor.Vault + Supavisor.Vault, + {Cachex, name: Supavisor.Cache}, + # Start the Endpoint (http/https) + SupavisorWeb.Endpoint ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/supavisor/client_handler.ex b/lib/supavisor/client_handler.ex index d122917b..9ef949ab 100644 --- a/lib/supavisor/client_handler.ex +++ b/lib/supavisor/client_handler.ex @@ -35,7 +35,7 @@ defmodule Supavisor.ClientHandler do @impl true def init(_), do: :ignore - def init(ref, trans, _opts) do + def init(ref, trans, opts) do Process.flag(:trap_exit, true) {:ok, sock} = :ranch.handshake(ref) @@ -54,7 +54,9 @@ defmodule Supavisor.ClientHandler do mode: nil, timeout: nil, ps: nil, - ssl: false + ssl: false, + auth_secrets: nil, + def_mode_type: opts.def_mode_type } :gen_statem.enter_loop(__MODULE__, [hibernate_after: 5_000], :exchange, data) @@ -106,17 +108,28 @@ defmodule Supavisor.ClientHandler do {user, external_id} = parse_user_info(hello.payload["user"]) Logger.metadata(project: external_id, user: user) - case Tenants.get_user(external_id, user) do - {:ok, user_info} -> - if user_info.enforce_ssl and !data.ssl do + sni_hostname = try_get_sni(sock) + + case Tenants.get_user(user, external_id, sni_hostname) do + {:ok, info} -> + if info.tenant.enforce_ssl and !data.ssl do Logger.error("Tenant is not allowed to connect without SSL, user #{user}") :ok = send_error(sock, "XX000", "SSL connection is required") {:stop, :normal, data} else - new_data = update_user_data(data, external_id, user_info) + new_data = update_user_data(data, info) + + case auth_secrets(info, user) do + {:ok, auth_secrets} -> + Logger.debug("Authentication method: #{inspect(auth_secrets)}") + {:keep_state, new_data, {:next_event, :internal, {:handle, auth_secrets}}} - {:keep_state, new_data, - {:next_event, :internal, {:handle, fn -> user_info.db_password end}}} + {:error, reason} -> + Logger.error("Authentication auth_secrets error: #{inspect(reason)}") + + :ok = send_error(sock, "XX000", "Authentication error") + {:stop, :normal, data} + end end {:error, reason} -> @@ -127,10 +140,10 @@ defmodule Supavisor.ClientHandler do end end - def handle_event(:internal, {:handle, pass}, _, %{sock: sock} = data) do - Logger.info("Handle exchange") + def handle_event(:internal, {:handle, {method, secrets}}, _, %{sock: sock} = data) do + Logger.info("Handle exchange, auth method: #{inspect(method)}") - case handle_exchange(sock, pass, data.ssl) do + case handle_exchange(sock, {method, secrets}, data.ssl) do {:error, reason} -> Logger.error("Exchange error: #{inspect(reason)}") msg = Server.exchange_message(:final, "e=#{reason}") @@ -138,17 +151,28 @@ defmodule Supavisor.ClientHandler do {:stop, :normal, data} - :ok -> + {:ok, client_key} -> + secrets = + if client_key do + fn -> + Map.put(secrets.(), :client_key, client_key) + end + else + nil + end + Logger.info("Exchange success") :ok = sock_send(sock, Server.authentication_ok()) - {:keep_state_and_data, {:next_event, :internal, :subscribe}} + + {:keep_state, %{data | auth_secrets: secrets}, {:next_event, :internal, :subscribe}} end end def handle_event(:internal, :subscribe, _, %{tenant: tenant, user_alias: db_alias} = data) do Logger.info("Subscribe to tenant #{inspect({tenant, db_alias})}") - with {:ok, tenant_sup} <- Supavisor.start(tenant, db_alias), + with {:ok, tenant_sup} <- + Supavisor.start(tenant, db_alias, data.auth_secrets, data.def_mode_type), {:ok, %{manager: manager, pool: pool}, ps} <- Supavisor.subscribe_global(node(tenant_sup), self(), tenant, db_alias) do Process.monitor(manager) @@ -328,7 +352,7 @@ defmodule Supavisor.ClientHandler do def parse_user_info(username) do case :binary.matches(username, ".") do [] -> - {nil, username} + {username, nil} matches -> {pos, _} = List.last(matches) @@ -352,53 +376,63 @@ defmodule Supavisor.ClientHandler do :undef end - @spec handle_exchange(sock(), fun(), boolean()) :: :ok | {:error, String.t()} - def handle_exchange({_, socket} = sock, password, ssl) do + @spec handle_exchange(sock(), {atom(), fun()}, boolean()) :: + {:ok, binary() | nil} | {:error, String.t()} + def handle_exchange({_, socket} = sock, {method, secrets}, ssl) do :ok = sock_send(sock, Server.auth_request()) + with {:ok, + %{ + tag: :password_message, + payload: {:scram_sha_256, %{"n" => user, "r" => nonce}} + }, _} <- receive_next(socket, "Timeout while waiting for the first password message"), + {:ok, signatures} = reply_first_exchange(sock, method, secrets, nonce, user, ssl), + {:ok, + %{ + tag: :password_message, + payload: {:first_msg_response, %{"p" => p}} + }, _} <- receive_next(socket, "Timeout while waiting for the second password message"), + {:ok, key} <- authenticate_exchange(method, secrets, signatures, p) do + message = "v=#{Base.encode64(signatures.server)}" + :ok = sock_send(sock, Server.exchange_message(:final, message)) + {:ok, key} + else + {:error, message} -> {:error, message} + other -> {:error, "Unexpected message #{inspect(other)}"} + end + end + + defp receive_next(socket, timeout_message) do receive do {_proto, ^socket, bin} -> - case Server.decode_pkt(bin) do - {:ok, - %{tag: :password_message, payload: {:scram_sha_256, %{"n" => user, "r" => nonce}}}, - _} -> - message = Server.exchange_first_message(nonce) - server_first_parts = :pgo_scram.parse_server_first(message, nonce) - channel = if ssl, do: "eSws", else: "biws" - - {client_final_message, server_proof} = - H.get_client_final(server_first_parts, nonce, user, password.(), channel) - - :ok = sock_send(sock, Server.exchange_message(:first, message)) - - receive do - {_proto, ^socket, bin} -> - case Server.decode_pkt(bin) do - {:ok, %{tag: :password_message, payload: {:first_msg_response, %{"p" => p}}}, _} -> - if p == List.last(client_final_message) do - message = "v=#{Base.encode64(server_proof)}" - :ok = sock_send(sock, Server.exchange_message(:final, message)) - else - {:error, "Invalid client signature"} - end - - other -> - {:error, "Unexpected message #{inspect(other)}"} - end - - other -> - {:error, "Unexpected message #{inspect(other)}"} - after - 15_000 -> - {:error, "Timeout while waiting for the second password message"} - end - - other -> - {:error, "Unexpected message #{inspect(other)}"} - end + Server.decode_pkt(bin) after - 15_000 -> - {:error, "Timeout while waiting for the first password message"} + 15_000 -> {:error, timeout_message} + end + end + + defp reply_first_exchange(sock, method, secrets, nonce, user, ssl) do + channel = if ssl, do: "eSws", else: "biws" + {message, signatures} = exchange_first(method, secrets, nonce, user, channel) + :ok = sock_send(sock, Server.exchange_message(:first, message)) + {:ok, signatures} + end + + defp authenticate_exchange(:password, _secrets, signatures, p) do + if p == signatures.client do + {:ok, nil} + else + {:error, "Invalid client signature"} + end + end + + defp authenticate_exchange(:auth_query, secrets, signatures, p) do + client_key = :crypto.exor(p |> Base.decode64!(), signatures.client) + + if H.hash(client_key) == secrets.().stored_key do + {:ok, client_key} + else + {:error, "Invalid client signature"} end end @@ -426,14 +460,14 @@ defmodule Supavisor.ClientHandler do defp handle_db_pid(:session, _, db_pid), do: db_pid - defp update_user_data(data, external_id, user_info) do + defp update_user_data(data, info) do %{ data - | tenant: external_id, - user_alias: user_info.db_user_alias, - mode: user_info.mode_type, - timeout: user_info.pool_checkout_timeout, - ps: user_info.default_parameter_status + | tenant: info.tenant.external_id, + user_alias: info.user.db_user_alias, + mode: info.user.mode_type, + timeout: info.user.pool_checkout_timeout, + ps: info.tenant.default_parameter_status } end @@ -453,4 +487,114 @@ defmodule Supavisor.ClientHandler do mod = if mod == :gen_tcp, do: :inet, else: mod mod.setopts(sock, opts) end + + @spec auth_secrets(map, String.t()) :: + {:ok, {:password | :auth_query, fun()}} | {:error, term()} + def auth_secrets(%{user: user, tenant: %{require_user: true}}, _) do + {:ok, {:password, fn -> user.db_password end}} + end + + def auth_secrets(%{user: user, tenant: tenant} = info, db_user) do + cache_key = {:secrets, tenant.external_id, user} + + case Cachex.fetch(Supavisor.Cache, cache_key, fn _key -> + {:commit, {:cached, get_secrets(info, db_user)}, ttl: 5_000} + end) do + {_, {:cached, value}} -> + value + + {_, {:cached, value}, _} -> + value + end + end + + @spec get_secrets(map, String.t()) :: {:ok, {:auth_query, fun()}} | {:error, term()} + def get_secrets(%{user: user, tenant: tenant}, db_user) do + ssl_opts = + if tenant.upstream_ssl and tenant.upstream_verify == "peer" do + [ + {:verify, :verify_peer}, + {:cacerts, [H.upstream_cert(tenant.upstream_tls_ca)]}, + {:customize_hostname_check, [{:match_fun, fn _, _ -> true end}]} + ] + end + + {:ok, conn} = + Postgrex.start_link( + hostname: tenant.db_host, + port: tenant.db_port, + database: tenant.db_database, + password: user.db_password, + username: user.db_user, + ssl: tenant.upstream_ssl, + socket_options: [ + H.ip_version(tenant.ip_version, tenant.db_host) + ], + ssl_opts: ssl_opts || [] + ) + + resp = + case H.get_user_secret(conn, tenant.auth_query, db_user) do + {:ok, secret} -> + {:ok, {:auth_query, fn -> secret end}} + + {:error, reason} -> + {:error, reason} + end + + GenServer.stop(conn, :normal) + resp + end + + @spec exchange_first(:password | :auth_query, fun(), binary(), binary(), binary()) :: + {binary(), map()} + defp exchange_first(:password, secret, nonce, user, channel) do + message = Server.exchange_first_message(nonce) + server_first_parts = H.parse_server_first(message, nonce) + + {client_final_message, server_proof} = + H.get_client_final( + :password, + secret.(), + server_first_parts, + nonce, + user, + channel + ) + + sings = %{ + client: List.last(client_final_message), + server: server_proof + } + + {message, sings} + end + + defp exchange_first(:auth_query, secret, nonce, user, channel) do + secret = secret.() + message = Server.exchange_first_message(nonce, secret.salt) + server_first_parts = H.parse_server_first(message, nonce) + + sings = + H.signatures( + secret.stored_key, + secret.server_key, + server_first_parts, + nonce, + user, + channel + ) + + {message, sings} + end + + @spec try_get_sni(sock()) :: String.t() | nil + def try_get_sni({:ssl, sock}) do + case :ssl.connection_information(sock, [:sni_hostname]) do + {:ok, [sni_hostname: sni]} -> List.to_string(sni) + _ -> nil + end + end + + def try_get_sni(_), do: nil end diff --git a/lib/supavisor/db_handler.ex b/lib/supavisor/db_handler.ex index 0a2d4ef1..5a8b06c0 100644 --- a/lib/supavisor/db_handler.ex +++ b/lib/supavisor/db_handler.ex @@ -13,6 +13,7 @@ defmodule Supavisor.DbHandler do @behaviour :gen_statem alias Supavisor.ClientHandler, as: Client + alias Supavisor.Helpers, as: H alias Supavisor.{Protocol.Server, Monitoring.Telem} @reconnect_timeout 2_500 @@ -71,6 +72,7 @@ defmodule Supavisor.DbHandler do case try_ssl_handshake({:gen_tcp, sock}, auth) do {:ok, sock} -> + # TODO: fix user name case send_startup(sock, auth) do :ok -> :ok = activate(sock) @@ -115,11 +117,8 @@ defmodule Supavisor.DbHandler do {:ok, req_method, _} -> Logger.debug("SASL method #{inspect(req_method)}") nonce = :pgo_scram.get_nonce(16) - - client_first = - data.auth.user - |> :pgo_scram.get_client_first(nonce) - + user = get_user(data.auth) + client_first = :pgo_scram.get_client_first(user, nonce) client_first_size = IO.iodata_length(client_first) sasl_initial_response = [ @@ -140,6 +139,26 @@ defmodule Supavisor.DbHandler do {ps, :authentication_sasl, nonce} + %{payload: {:authentication_server_first_message, server_first}}, {ps, _} + when data.auth.require_user == false -> + nonce = data.nonce + server_first_parts = H.parse_server_first(server_first, nonce) + + {client_final_message, server_proof} = + H.get_client_final( + :auth_query, + data.auth.secrets.(), + server_first_parts, + nonce, + data.auth.secrets.().user, + "biws" + ) + + bin = :pgo_protocol.encode_scram_response_message(client_final_message) + :ok = sock_send(data.sock, bin) + + {ps, :authentication_server_first_message, server_proof} + %{payload: {:authentication_server_first_message, server_first}}, {ps, _} -> nonce = data.nonce server_first_parts = :pgo_scram.parse_server_first(server_first, nonce) @@ -327,9 +346,11 @@ defmodule Supavisor.DbHandler do @spec send_startup(sock(), map()) :: :ok | {:error, term} defp send_startup(sock, auth) do + user = get_user(auth) + msg = :pgo_protocol.encode_startup_message([ - {"user", auth.user}, + {"user", user}, {"database", auth.database}, {"application_name", auth.application_name} ]) @@ -350,4 +371,12 @@ defmodule Supavisor.DbHandler do defp activate({:ssl, sock}) do :ssl.setopts(sock, active: true) end + + defp get_user(auth) do + if auth.require_user do + auth.user + else + auth.secrets.().user + end + end end diff --git a/lib/supavisor/helpers.ex b/lib/supavisor/helpers.ex index 167d31f4..05c1c21b 100644 --- a/lib/supavisor/helpers.ex +++ b/lib/supavisor/helpers.ex @@ -2,7 +2,25 @@ defmodule Supavisor.Helpers do @moduledoc false @spec check_creds_get_ver(map) :: {:ok, String.t()} | {:error, String.t()} + + def check_creds_get_ver(%{"require_user" => false} = params) do + cond do + length(params["users"]) != 1 -> + {:error, "Can't use 'require_user' and 'auth_query' with multiple users"} + + !hd(params["users"])["is_manager"] -> + {:error, "Can't use 'require_user' and 'auth_query' with non-manager user"} + + true -> + do_check_creds_get_ver(params) + end + end + def check_creds_get_ver(params) do + do_check_creds_get_ver(params) + end + + def do_check_creds_get_ver(params) do Enum.reduce_while(params["users"], {nil, nil}, fn user, _ -> upstream_ssl? = !!params["upstream_ssl"] @@ -33,7 +51,17 @@ defmodule Supavisor.Helpers do Postgrex.query(conn, "select version()", []) |> case do {:ok, %{rows: [[version]]}} -> - {:cont, {:ok, version}} + if !params["require_user"] do + case get_user_secret(conn, params["auth_query"], user["db_user"]) do + {:ok, _} -> + {:halt, {:ok, version}} + + {:error, reason} -> + {:halt, {:error, reason}} + end + else + {:cont, {:ok, version}} + end {:error, reason} -> {:halt, {:error, "Can't connect the user #{user["db_user"]}: #{inspect(reason)}"}} @@ -51,6 +79,53 @@ defmodule Supavisor.Helpers do end end + @spec get_user_secret(pid(), String.t(), String.t()) :: {:ok, map()} | {:error, String.t()} + def get_user_secret(conn, auth_query, user) do + try do + Postgrex.query!(conn, auth_query, [user]) + catch + _error, reason -> + {:error, "Authentication query failed: #{inspect(reason)}"} + end + |> case do + %{columns: [_, _], rows: [[^user, secret]]} -> + parse_secret(secret, user) + + %{columns: [_, _], rows: []} -> + {:error, + "There is no user '#{user}' in the database. Please create it or change the user in the config"} + + %{columns: colums} -> + {:error, + "Authentification query returned wrong format. Should be two columns: user and secret, but got: #{inspect(colums)}"} + + {:error, reason} -> + {:error, reason} + end + end + + @spec parse_secret(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()} + def parse_secret("SCRAM-SHA-256" <> _ = secret, user) do + # $:$: + case Regex.run(~r/^(.+)\$(\d+):(.+)\$(.+):(.+)$/, secret) do + [_, digest, iterations, salt, stored_key, server_key] -> + {:ok, + %{ + digest: digest, + iterations: String.to_integer(iterations), + salt: salt, + stored_key: Base.decode64!(stored_key), + server_key: Base.decode64!(server_key), + user: user + }} + + _ -> + {:error, "Can't parse secret"} + end + end + + def parse_postgres_secret(_), do: {:error, "Digest not supported"} + ## Internal functions @doc """ @@ -151,19 +226,27 @@ defmodule Supavisor.Helpers do Application.get_env(:supavisor, :global_downstream_key) end - @spec get_client_final(term(), binary(), iodata(), iodata(), binary()) :: {iodata(), binary()} - def get_client_final(srv_first, client_nonce, user_name, password, channel) do + @spec get_client_final(:password | :auth_query, map(), map(), binary(), binary(), binary()) :: + {iolist(), binary()} + def get_client_final( + :password, + secrets, + srv_first, + client_nonce, + user_name, + channel + ) do channel_binding = "c=#{channel}" - nonce = ["r=", srv_first[:nonce]] + nonce = ["r=", srv_first.nonce] - salt = srv_first[:salt] - i = srv_first[:i] + salt = srv_first.salt + i = srv_first.i - salted_password = :pgo_scram.hi(:pgo_sasl_prep_profile.validate(password), salt, i) + salted_password = :pgo_scram.hi(:pgo_sasl_prep_profile.validate(secrets), salt, i) client_key = :pgo_scram.hmac(salted_password, "Client Key") stored_key = :pgo_scram.h(client_key) client_first_bare = [<<"n=">>, user_name, <<",r=">>, client_nonce] - server_first = srv_first[:raw] + server_first = srv_first.raw client_final_without_proof = [channel_binding, ",", nonce] auth_message = [client_first_bare, ",", server_first, ",", client_final_without_proof] client_signature = :pgo_scram.hmac(stored_key, auth_message) @@ -174,4 +257,50 @@ defmodule Supavisor.Helpers do {[client_final_without_proof, ",p=", Base.encode64(client_proof)], server_signature} end + + def get_client_final( + :auth_query, + secrets, + srv_first, + client_nonce, + user_name, + channel + ) do + channel_binding = "c=#{channel}" + nonce = ["r=", srv_first.nonce] + + client_first_bare = [<<"n=">>, user_name, <<",r=">>, client_nonce] + server_first = srv_first.raw + client_final_without_proof = [channel_binding, ",", nonce] + auth_message = [client_first_bare, ",", server_first, ",", client_final_without_proof] + client_signature = :pgo_scram.hmac(secrets.stored_key, auth_message) + client_proof = :pgo_scram.bin_xor(secrets.client_key, client_signature) + + server_signature = :pgo_scram.hmac(secrets.server_key, auth_message) + + {[client_final_without_proof, ",p=", Base.encode64(client_proof)], server_signature} + end + + def signatures(stored_key, server_key, srv_first, client_nonce, user_name, channel) do + channel_binding = "c=#{channel}" + nonce = ["r=", srv_first.nonce] + client_first_bare = [<<"n=">>, user_name, <<",r=">>, client_nonce] + server_first = srv_first.raw + client_final_without_proof = [channel_binding, ",", nonce] + auth_message = [client_first_bare, ",", server_first, ",", client_final_without_proof] + + %{ + client: :pgo_scram.hmac(stored_key, auth_message), + server: :pgo_scram.hmac(server_key, auth_message) + } + end + + def hash(bin) do + :crypto.hash(:sha256, bin) + end + + @spec parse_server_first(binary(), binary()) :: map() + def parse_server_first(message, nonce) do + :pgo_scram.parse_server_first(message, nonce) |> Map.new() + end end diff --git a/lib/supavisor/protocol/server.ex b/lib/supavisor/protocol/server.ex index 9b75dcfe..01b00ef4 100644 --- a/lib/supavisor/protocol/server.ex +++ b/lib/supavisor/protocol/server.ex @@ -254,10 +254,17 @@ defmodule Supavisor.Protocol.Server do @auth_request end - def exchange_first_message(nonce) do - secret = :pgo_scram.get_nonce(16) |> Base.encode64() + @spec exchange_first_message(binary(), binary() | boolean(), pos_integer()) :: binary() + def exchange_first_message(nonce, salt \\ false, iterations \\ 4096) do + secret = + if salt do + salt + else + :pgo_scram.get_nonce(16) |> Base.encode64() + end + server_nonce = :pgo_scram.get_nonce(16) |> Base.encode64() - "r=#{nonce <> server_nonce},s=#{secret},i=4096" + "r=#{nonce <> server_nonce},s=#{secret},i=#{iterations}" end @spec exchange_message(:first | :final, binary()) :: iodata() diff --git a/lib/supavisor/tenants.ex b/lib/supavisor/tenants.ex index 4c54951b..201ec8a7 100644 --- a/lib/supavisor/tenants.ex +++ b/lib/supavisor/tenants.ex @@ -43,31 +43,53 @@ defmodule Supavisor.Tenants do Tenant |> Repo.get_by(external_id: external_id) |> Repo.preload(:users) end - @spec get_user(String.t(), String.t() | nil) :: + @spec get_user(String.t(), String.t() | nil, String.t() | nil) :: {:ok, map()} | {:error, :not_found | :multiple_results} - def get_user(external_id, user) do - query = build_user_query(external_id, user) + def get_user(user, external_id, sni_hostname) do + query = build_user_query(user, external_id, sni_hostname) case Repo.all(query) do nil -> {:error, :not_found} - [[db_alias, pass, mode, timeout, ps, enforce_ssl]] -> - {:ok, - %{ - db_password: pass, - db_user_alias: db_alias, - mode_type: mode, - pool_checkout_timeout: timeout, - default_parameter_status: ps, - enforce_ssl: enforce_ssl - }} + [{%User{}, %Tenant{}} = {user, tenant}] -> + {:ok, %{user: user, tenant: tenant}} + + [] -> + get_auth_user(external_id, sni_hostname) _ -> {:error, :multiple_results} end end + def get_auth_user(external_id, sni_hostname) do + query = + if sni_hostname do + from(u in User, + join: t in Tenant, + on: u.tenant_external_id == t.external_id, + where: t.sni_hostname == ^sni_hostname and t.require_user == false, + select: {u, t} + ) + else + from(t in Tenant, + join: u in User, + on: u.tenant_external_id == t.external_id, + where: t.external_id == ^external_id and t.require_user == false, + select: {u, t} + ) + end + + case Repo.all(query) do + [{%User{}, %Tenant{}} = {user, tenant}] -> + {:ok, %{user: user, tenant: tenant}} + + _ -> + {:error, :not_found} + end + end + def get_pool_config(external_id, user) do query = from(a in User, @@ -247,29 +269,32 @@ defmodule Supavisor.Tenants do User.changeset(user, attrs) end - @spec build_user_query(String.t(), String.t() | nil) :: Ecto.Queryable.t() - defp build_user_query(external_id, user) do - query = - from(u in User, - join: t in Tenant, - on: u.tenant_external_id == t.external_id, - where: u.tenant_external_id == ^external_id, - select: [ - u.db_user_alias, - u.db_password, - u.mode_type, - u.pool_checkout_timeout, - t.default_parameter_status, - t.enforce_ssl - ] - ) - - if user do - from(u in query, - where: u.db_user_alias == ^user - ) + @spec build_user_query(String.t(), String.t() | nil, String.t() | nil) :: Ecto.Queryable.t() + defp build_user_query(user, external_id, sni_hostname) do + if sni_hostname do + query = + from(t in Tenant, + join: u in User, + on: u.tenant_external_id == t.external_id, + where: t.sni_hostname == ^sni_hostname, + select: {u, t} + ) else - query + query = + from(u in User, + join: t in Tenant, + on: u.tenant_external_id == t.external_id, + where: u.tenant_external_id == ^external_id, + select: {u, t} + ) + + if user do + from(u in query, + where: u.db_user_alias == ^user + ) + else + query + end end end end diff --git a/lib/supavisor/tenants/tenant.ex b/lib/supavisor/tenants/tenant.ex index 0631b13e..f5671f48 100644 --- a/lib/supavisor/tenants/tenant.ex +++ b/lib/supavisor/tenants/tenant.ex @@ -21,6 +21,10 @@ defmodule Supavisor.Tenants.Tenant do field(:upstream_verify, Ecto.Enum, values: [:none, :peer]) field(:upstream_tls_ca, :binary) field(:enforce_ssl, :boolean, default: false) + field(:require_user, :boolean, default: false) + field(:auth_query, :string) + field(:default_pool_size, :integer, default: 15) + field(:sni_hostname, :string) has_many(:users, User, foreign_key: :tenant_external_id, @@ -45,7 +49,11 @@ defmodule Supavisor.Tenants.Tenant do :upstream_ssl, :upstream_verify, :upstream_tls_ca, - :enforce_ssl + :enforce_ssl, + :require_user, + :auth_query, + :default_pool_size, + :sni_hostname ]) |> check_constraint(:upstream_ssl, name: :upstream_constraints, prefix: "_supavisor") |> check_constraint(:upstream_verify, name: :upstream_constraints, prefix: "_supavisor") @@ -54,7 +62,8 @@ defmodule Supavisor.Tenants.Tenant do :external_id, :db_host, :db_port, - :db_database + :db_database, + :require_user ]) |> unique_constraint([:external_id]) |> cast_assoc(:users, with: &User.changeset/2) diff --git a/lib/supavisor_web/views/tenant_view.ex b/lib/supavisor_web/views/tenant_view.ex index bac05254..e06d5d75 100644 --- a/lib/supavisor_web/views/tenant_view.ex +++ b/lib/supavisor_web/views/tenant_view.ex @@ -22,6 +22,9 @@ defmodule SupavisorWeb.TenantView do upstream_ssl: tenant.upstream_ssl, upstream_verify: tenant.upstream_verify, enforce_ssl: tenant.enforce_ssl, + require_user: tenant.require_user, + auth_query: tenant.auth_query, + sni_hostname: tenant.sni_hostname, users: render_many(tenant.users, UserView, "user.json") } end diff --git a/lib/supavisor_web/ws_proxy.ex b/lib/supavisor_web/ws_proxy.ex index 523269e1..f4958dbc 100644 --- a/lib/supavisor_web/ws_proxy.ex +++ b/lib/supavisor_web/ws_proxy.ex @@ -60,7 +60,7 @@ defmodule SupavisorWeb.WsProxy do @spec connect_local() :: {:ok, port()} | {:error, term()} defp connect_local() do - proxy_port = Application.fetch_env!(:supavisor, :proxy_port) + proxy_port = Application.fetch_env!(:supavisor, :proxy_port_transaction) :gen_tcp.connect('localhost', proxy_port, [:binary, packet: :raw, active: true]) end end diff --git a/mix.exs b/mix.exs index 49fe392b..f1202bb8 100644 --- a/mix.exs +++ b/mix.exs @@ -59,6 +59,7 @@ defmodule Supavisor.MixProject do {:libcluster, "~> 3.3.1"}, {:logflare_logger_backend, github: "Logflare/logflare_logger_backend", tag: "v0.11.1-rc.1"}, {:distillery, "~> 2.1"}, + {:cachex, "~> 3.6"}, # pooller {:poolboy, "~> 1.5.2"}, diff --git a/mix.lock b/mix.lock index cba7f7bd..3f68bc15 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,7 @@ "bertex": {:hex, :bertex, "1.3.0", "0ad0df9159b5110d9d2b6654f72fbf42a54884ef43b6b651e6224c0af30ba3cb", [:mix], [], "hexpm", "0a5d5e478bb5764b7b7bae37cae1ca491200e58b089df121a2fe1c223d8ee57a"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "burrito": {:git, "https://github.com/burrito-elixir/burrito.git", "dfb3e39d478944c6a17fc37c24ed3ad4a8cc2af0", []}, + "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "castore": {:hex, :castore, "1.0.2", "0c6292ecf3e3f20b7c88408f00096337c4bfd99bd46cc2fe63413ddbe45b3573", [:mix], [], "hexpm", "40b2dd2836199203df8500e4a270f10fc006cc95adc8a319e148dc3077391d96"}, "cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"}, "cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"}, @@ -21,6 +22,7 @@ "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"}, "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "etso": {:hex, :etso, "1.1.0", "ddbf5417522ecc5f9544a5daeb67fc5f7509a5edb7f65add85a530dc35f80ec5", [:mix], [{:ecto, "~> 3.8.3", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "aa74f6bd76fb444aaa94554c668d637eedd6d71c0a9887ef973437ebe6645368"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, @@ -28,6 +30,7 @@ "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "joken": {:hex, :joken, "2.5.0", "09be497d804b8115eb6f07615cef2e60c2a1008fb89dc0aef0d4c4b4609b99aa", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "22b25c89617c5ed8ca7b31026340a25ea0f9ca7160f9706b79be9ed81fdf74e7"}, "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"}, + "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "libcluster": {:hex, :libcluster, "3.3.2", "84c6ebfdc72a03805955abfb5ff573f71921a3e299279cc3445445d5af619ad1", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8b691ce8185670fc8f3fc0b7ed59eff66c6889df890d13411f8f1a0e6871d8a5"}, "logflare_api_client": {:hex, :logflare_api_client, "0.3.5", "c427ebf65a8402d68b056d4a5ef3e1eb3b90c0ad1d0de97d1fe23807e0c1b113", [:mix], [{:bertex, "~> 1.3", [hex: :bertex, repo: "hexpm", optional: false]}, {:finch, "~> 0.10", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "16d29abcb80c4f72745cdf943379da02a201504813c3aa12b4d4acb0302b7723"}, "logflare_logger_backend": {:git, "https://github.com/Logflare/logflare_logger_backend.git", "5c117de97376c560c82946a057b5778924e4e205", [tag: "v0.11.1-rc.1"]}, @@ -59,6 +62,7 @@ "prom_ex": {:hex, :prom_ex, "1.8.0", "662615e1d2f2ab3e0dc13a51c92ad0ccfcab24336a90cb9b114ee1bce9ef88aa", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "3eea763dfa941e25de50decbf17a6a94dbd2270e7b32f88279aa6e9bbb8e23e7"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "req": {:hex, :req, "0.3.8", "e254074435c970b1d7699777f1a8466acbacab5e6ba4a264d35053bf52c03467", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a17244d18a7fbf3e9892c38c10628224f6f7974fd364392ca0d85f91e3cc8251"}, + "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "syn": {:hex, :syn, "3.3.0", "4684a909efdfea35ce75a9662fc523e4a8a4e8169a3df275e4de4fa63f99c486", [:rebar3], [], "hexpm", "e58ee447bc1094bdd21bf0acc102b1fbf99541a508cd48060bf783c245eaf7d6"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, @@ -67,6 +71,7 @@ "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, + "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, "websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"}, "websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"}, } diff --git a/priv/repo/migrations/20230714153019_add_auth_query.exs b/priv/repo/migrations/20230714153019_add_auth_query.exs new file mode 100644 index 00000000..befa2ebe --- /dev/null +++ b/priv/repo/migrations/20230714153019_add_auth_query.exs @@ -0,0 +1,30 @@ +defmodule Supavisor.Repo.Migrations.AddAuthQuery do + use Ecto.Migration + + def up do + alter table("tenants", prefix: "_supavisor") do + add(:require_user, :boolean, null: false, default: false) + add(:auth_query, :string, null: true) + add(:default_pool_size, :integer, null: false, default: 15) + end + + auth_query_constraints = """ + (require_user = true) OR (require_user = false AND auth_query IS NOT NULL) + """ + + create( + constraint("tenants", :auth_query_constraints, + check: auth_query_constraints, + prefix: "_supavisor" + ) + ) + end + + def down do + alter table("tenants", prefix: "_supavisor") do + remove(:auth_query) + end + + drop(constraint("tenants", "auth_query_constraints", prefix: "_supavisor")) + end +end diff --git a/priv/repo/migrations/20230718175315_add_sni_host.exs b/priv/repo/migrations/20230718175315_add_sni_host.exs new file mode 100644 index 00000000..3aca02c4 --- /dev/null +++ b/priv/repo/migrations/20230718175315_add_sni_host.exs @@ -0,0 +1,9 @@ +defmodule Supavisor.Repo.Migrations.AddSniHost do + use Ecto.Migration + + def change do + alter table("tenants", prefix: "_supavisor") do + add(:sni_hostname, :string, null: true) + end + end +end diff --git a/priv/repo/seeds_after_migration.exs b/priv/repo/seeds_after_migration.exs index 6c5b6f77..48aee69d 100644 --- a/priv/repo/seeds_after_migration.exs +++ b/priv/repo/seeds_after_migration.exs @@ -25,6 +25,7 @@ end db_database: db_conf[:database], external_id: tenant, default_parameter_status: %{server_version: version}, + require_user: true, users: [ %{ "db_user" => db_conf[:username], diff --git a/test/integration/proxy_test.exs b/test/integration/proxy_test.exs index 377ec190..90c41028 100644 --- a/test/integration/proxy_test.exs +++ b/test/integration/proxy_test.exs @@ -10,7 +10,7 @@ defmodule Supavisor.Integration.ProxyTest do {:ok, proxy} = Postgrex.start_link( hostname: db_conf[:hostname], - port: Application.get_env(:supavisor, :proxy_port), + port: Application.get_env(:supavisor, :proxy_port_transaction), database: db_conf[:database], password: db_conf[:password], username: db_conf[:username] <> "." <> @tenant @@ -32,7 +32,7 @@ defmodule Supavisor.Integration.ProxyTest do db_conf = Application.get_env(:supavisor, Repo) url = - "postgresql://#{db_conf[:username] <> "." <> @tenant}:no_pass@#{db_conf[:hostname]}:#{Application.get_env(:supavisor, :proxy_port)}/postgres" + "postgresql://#{db_conf[:username] <> "." <> @tenant}:no_pass@#{db_conf[:hostname]}:#{Application.get_env(:supavisor, :proxy_port_transaction)}/postgres" {result, _} = System.cmd("psql", [url], stderr_to_stdout: true) assert result =~ "error received from server in SCRAM exchange: Invalid client signature" @@ -140,7 +140,9 @@ defmodule Supavisor.Integration.ProxyTest do # end test "http to proxy server returns 200 OK" do - assert :httpc.request("http://localhost:#{Application.get_env(:supavisor, :proxy_port)}") == + assert :httpc.request( + "http://localhost:#{Application.get_env(:supavisor, :proxy_port_transaction)}" + ) == {:ok, {{'HTTP/1.1', 204, 'OK'}, [], []}} end @@ -148,7 +150,7 @@ defmodule Supavisor.Integration.ProxyTest do db_conf = Application.get_env(:supavisor, Repo) url = - "postgresql://transaction.proxy_tenant:#{db_conf[:password]}@#{db_conf[:hostname]}:#{Application.get_env(:supavisor, :proxy_port)}/postgres" + "postgresql://transaction.proxy_tenant:#{db_conf[:password]}@#{db_conf[:hostname]}:#{Application.get_env(:supavisor, :proxy_port_transaction)}/postgres" psql_pid = spawn(fn -> System.cmd("psql", [url]) end) diff --git a/test/supavisor/client_handler_test.exs b/test/supavisor/client_handler_test.exs index 86b05714..e50fae78 100644 --- a/test/supavisor/client_handler_test.exs +++ b/test/supavisor/client_handler_test.exs @@ -11,10 +11,10 @@ defmodule Supavisor.ClientHandlerTest do assert external_id == "external_id" end - test "username consists only of external_id" do - username = "external_id" - {nil, external_id} = ClientHandler.parse_user_info(username) - assert external_id == "external_id" + test "username consists only of username" do + username = "username" + {user, nil} = ClientHandler.parse_user_info(username) + assert username == "username" end end end diff --git a/test/supavisor/db_handler_test.exs b/test/supavisor/db_handler_test.exs index 6045920f..d65c3cb4 100644 --- a/test/supavisor/db_handler_test.exs +++ b/test/supavisor/db_handler_test.exs @@ -35,6 +35,7 @@ defmodule Supavisor.DbHandlerTest do host: "host", port: 0, user: "some user", + require_user: true, database: "some database", application_name: "some application name", ip_version: :inet @@ -51,6 +52,7 @@ defmodule Supavisor.DbHandlerTest do host: "host", port: 0, user: "some user", + require_user: true, ip_version: :inet }, sock: {:gen_tcp, :sock} diff --git a/test/supavisor/prom_ex_test.exs b/test/supavisor/prom_ex_test.exs index 333d94c1..20b917d7 100644 --- a/test/supavisor/prom_ex_test.exs +++ b/test/supavisor/prom_ex_test.exs @@ -13,7 +13,7 @@ defmodule Supavisor.PromExTest do {:ok, proxy} = Postgrex.start_link( hostname: db_conf[:hostname], - port: Application.get_env(:supavisor, :proxy_port), + port: Application.get_env(:supavisor, :proxy_port_transaction), database: db_conf[:database], password: db_conf[:password], username: db_conf[:username] <> "." <> @tenant diff --git a/test/supavisor/syn_handler_test.exs b/test/supavisor/syn_handler_test.exs index ea6bf690..ead6d8a4 100644 --- a/test/supavisor/syn_handler_test.exs +++ b/test/supavisor/syn_handler_test.exs @@ -10,7 +10,7 @@ defmodule Supavisor.SynHandlerTest do test "resolving conflict" do node2 = :"secondary@127.0.0.1" - {:ok, pid2} = :erpc.call(node2, Supavisor, :start, [@tenant, @user]) + {:ok, pid2} = :erpc.call(node2, Supavisor, :start, [@tenant, @user, fn -> :ok end]) Process.sleep(500) assert pid2 == Supavisor.get_global_sup(@tenant, @user) assert node(pid2) == node2 @@ -18,7 +18,7 @@ defmodule Supavisor.SynHandlerTest do Process.sleep(500) assert nil == Supavisor.get_global_sup(@tenant, @user) - {:ok, pid1} = Supavisor.start(@tenant, @user) + {:ok, pid1} = Supavisor.start(@tenant, @user, fn -> :ok end) assert pid1 == Supavisor.get_global_sup(@tenant, @user) assert node(pid1) == node() diff --git a/test/supavisor/tenants_test.exs b/test/supavisor/tenants_test.exs index 0a5dc7ab..977e24e2 100644 --- a/test/supavisor/tenants_test.exs +++ b/test/supavisor/tenants_test.exs @@ -25,6 +25,7 @@ defmodule Supavisor.TenantsTest do "db_user" => "some db_user", "db_password" => "some db_password", "pool_size" => 3, + "require_user" => true, "mode_type" => "transaction" } @@ -34,6 +35,7 @@ defmodule Supavisor.TenantsTest do db_database: "some db_database", external_id: "dev_tenant", default_parameter_status: %{"server_version" => "15.0"}, + require_user: true, users: [user_valid_attrs] } diff --git a/test/supavisor_web/controllers/tenant_controller_test.exs b/test/supavisor_web/controllers/tenant_controller_test.exs index 090dd631..fdd4f908 100644 --- a/test/supavisor_web/controllers/tenant_controller_test.exs +++ b/test/supavisor_web/controllers/tenant_controller_test.exs @@ -21,6 +21,7 @@ defmodule SupavisorWeb.TenantControllerTest do db_host: "some db_host", db_port: 42, external_id: "dev_tenant", + require_user: true, users: [@user_valid_attrs] } @update_attrs %{ @@ -28,6 +29,7 @@ defmodule SupavisorWeb.TenantControllerTest do db_host: "some updated db_host", db_port: 43, external_id: "dev_tenant", + require_user: true, users: [@user_valid_attrs] } @invalid_attrs %{ diff --git a/test/support/cluster.ex b/test/support/cluster.ex index f0b5897d..6847111a 100644 --- a/test/support/cluster.ex +++ b/test/support/cluster.ex @@ -8,7 +8,7 @@ defmodule Supavisor.Support.Cluster do for {key, val} <- Application.get_all_env(app_name) do val = case {app_name, key} do - {:supavisor, :proxy_port} -> + {:supavisor, :proxy_port_transaction} -> Application.get_env(:supavisor, :secondary_proxy_port) {:supavisor, SupavisorWeb.Endpoint} -> diff --git a/test/support/fixtures/tenants_fixtures.ex b/test/support/fixtures/tenants_fixtures.ex index 0be2ad38..af4a5d73 100644 --- a/test/support/fixtures/tenants_fixtures.ex +++ b/test/support/fixtures/tenants_fixtures.ex @@ -16,6 +16,7 @@ defmodule Supavisor.TenantsFixtures do db_port: 42, external_id: "dev_tenant", default_parameter_status: %{"server_version" => "15.0"}, + require_user: true, users: [ %{ "db_user" => "postgres",