Skip to content

Commit

Permalink
Prod deploy (#178)
Browse files Browse the repository at this point in the history
* chore: update GH actions dependencies (#166)

* chore: bump ecto_sql and logflare_logger_backend (#170)

* chore: bump ecto_sql and logflare_logger_backend

* feat: add an option to set the pool's strategy, default is fifo (#173)

* feat: support auth_query with md5 (#175)

* feat: support auth_query with md5
* support unicode in a username, separate auth_request for scram and md5

* chore:bump version (#176)

fix a version for #175

* fix: error message during md5 auth (#177)

---------

Co-authored-by: divit <[email protected]>
  • Loading branch information
abc3 and delgado3d authored Oct 10, 2023
1 parent d4c6d65 commit 03c012a
Show file tree
Hide file tree
Showing 19 changed files with 179 additions and 77 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
packages: write
id-token: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup elixir
id: beam
uses: erlef/setup-beam@v1
Expand All @@ -24,7 +24,7 @@ jobs:
elixir-version: 1.14.5 # Define the elixir version [required]
version-type: strict
- name: Cache Mix
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
Expand All @@ -37,7 +37,7 @@ jobs:
- name: Create tarball
run: cd _build/prod/rel/ && tar -czvf ${{ secrets.TARBALL_REGIONS_PROD }}_supavisor_v$(cat ../../../VERSION)_$(date "+%s").tar.gz supavisor
- name: configure aws credentials - production
uses: aws-actions/configure-aws-credentials@v1
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
aws-region: "us-east-1"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/stage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
packages: write
id-token: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup elixir
id: beam
uses: erlef/setup-beam@v1
Expand All @@ -24,7 +24,7 @@ jobs:
elixir-version: 1.14.5 # Define the elixir version [required]
version-type: strict
- name: Cache Mix
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
Expand All @@ -37,7 +37,7 @@ jobs:
- name: Create tarball
run: cd _build/prod/rel/ && tar -czvf ${{ secrets.TARBALL_REGIONS_STAGE }}_supavisor_v$(cat ../../../VERSION)_$(date "+%s").tar.gz supavisor
- name: configure aws credentials - staging
uses: aws-actions/configure-aws-credentials@v1
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.DEV_AWS_ROLE }}
aws-region: "us-east-1"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/staging_linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup elixir
id: beam
uses: erlef/setup-beam@v1
with:
otp-version: 25.x # Define the OTP version [required]
elixir-version: 1.14.x # Define the elixir version [required]
- name: Cache Mix
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
Expand All @@ -37,7 +37,7 @@ jobs:
- name: Credo checks
run: mix credo --strict --mute-exit-status
- name: Retrieve PLT Cache
uses: actions/cache@v1
uses: actions/cache@v3
id: plt-cache
with:
path: priv/plts
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/version_updated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
name: Bump version
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Verify Versions Updated
uses: tj-actions/changed-files@v35
id: verify_changed_files
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.6
0.9.10
5 changes: 4 additions & 1 deletion lib/supavisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ defmodule Supavisor do
default_pool_size: def_pool_size,
default_max_clients: def_max_clients,
client_idle_timeout: client_idle_timeout,
default_pool_strategy: default_pool_strategy,
users: [
%{
db_user: db_user,
Expand Down Expand Up @@ -201,6 +202,7 @@ defmodule Supavisor do
upstream_verify: tenant_record.upstream_verify,
upstream_tls_ca: H.upstream_cert(tenant_record.upstream_tls_ca),
require_user: tenant_record.require_user,
method: method,
secrets: secrets
}

Expand All @@ -213,7 +215,8 @@ defmodule Supavisor do
mode: mode,
default_parameter_status: ps,
max_clients: max_clients,
client_idle_timeout: client_idle_timeout
client_idle_timeout: client_idle_timeout,
default_pool_strategy: default_pool_strategy
}

DynamicSupervisor.start_child(
Expand Down
76 changes: 56 additions & 20 deletions lib/supavisor/client_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ defmodule Supavisor.ClientHandler do
case decode_startup_packet(bin) do
{:ok, hello} ->
Logger.debug("Client startup message: #{inspect(hello)}")
{user, external_id} = parse_user_info(hello.payload["user"])
{user, external_id} = parse_user_info(hello.payload)
Logger.metadata(project: external_id, user: user, mode: data.mode)
{:keep_state, data, {:next_event, :internal, {:hello, {user, external_id}}}}

Expand Down Expand Up @@ -157,8 +157,15 @@ defmodule Supavisor.ClientHandler do

case handle_exchange(sock, {method, secrets}) do
{:error, reason} ->
Logger.error("Exchange error: #{inspect(reason)}")
msg = Server.exchange_message(:final, "e=#{reason}")
Logger.error("Exchange error: #{inspect(reason)} when method #{inspect(method)}")

msg =
if method == :auth_query_md5 do
Server.error_message("XX000", reason)
else
Server.exchange_message(:final, "e=#{reason}")
end

sock_send(sock, msg)

{:stop, :normal, data}
Expand Down Expand Up @@ -393,15 +400,19 @@ defmodule Supavisor.ClientHandler do

## Internal functions

@spec parse_user_info(String.t()) :: {String.t() | nil, String.t()}
def parse_user_info(username) do
case :binary.matches(username, ".") do
@spec parse_user_info(map) :: {String.t() | nil, String.t()}
def parse_user_info(%{"user" => user, "options" => %{"reference" => ref}}) do
{user, ref}
end

def parse_user_info(%{"user" => user}) do
case :binary.matches(user, ".") do
[] ->
{username, nil}
{user, nil}

matches ->
{pos, _} = List.last(matches)
{name, "." <> external_id} = String.split_at(username, pos)
{pos, 1} = List.last(matches)
<<name::size(pos)-binary, ?., external_id::binary>> = user
{name, external_id}
end
end
Expand Down Expand Up @@ -433,7 +444,10 @@ defmodule Supavisor.ClientHandler do
map =
fields
|> Enum.chunk_every(2)
|> Enum.map(fn [k, v] -> {k, v} end)
|> Enum.map(fn
["options" = k, v] -> {k, URI.decode_query(v)}
[k, v] -> {k, v}
end)
|> Map.new()

# We only do light validation on the fields in the payload. The only field we use at the
Expand All @@ -447,8 +461,25 @@ defmodule Supavisor.ClientHandler do
end

@spec handle_exchange(S.sock(), {atom(), fun()}) :: {:ok, binary() | nil} | {:error, String.t()}
def handle_exchange({_, socket} = sock, {:auth_query_md5 = method, secrets}) do
salt = :crypto.strong_rand_bytes(4)
:ok = sock_send(sock, Server.md5_request(salt))

with {:ok,
%{
tag: :password_message,
payload: {:md5, client_md5}
}, _} <- receive_next(socket, "Timeout while waiting for the md5 exchange"),
{:ok, key} <- authenticate_exchange(method, client_md5, secrets.().secret, salt) do
{:ok, key}
else
{:error, message} -> {:error, message}
other -> {:error, "Unexpected message #{inspect(other)}"}
end
end

def handle_exchange({_, socket} = sock, {method, secrets}) do
:ok = sock_send(sock, Server.auth_request())
:ok = sock_send(sock, Server.scram_request())

with {:ok,
%{
Expand All @@ -473,8 +504,7 @@ defmodule Supavisor.ClientHandler do

defp receive_next(socket, timeout_message) do
receive do
{_proto, ^socket, bin} ->
Server.decode_pkt(bin)
{_proto, ^socket, bin} -> Server.decode_pkt(bin)
after
15_000 -> {:error, timeout_message}
end
Expand All @@ -495,7 +525,7 @@ defmodule Supavisor.ClientHandler do
end

defp authenticate_exchange(:auth_query, secrets, signatures, p) do
client_key = :crypto.exor(p |> Base.decode64!(), signatures.client)
client_key = :crypto.exor(Base.decode64!(p), signatures.client)

if H.hash(client_key) == secrets.().stored_key do
{:ok, client_key}
Expand All @@ -504,6 +534,14 @@ defmodule Supavisor.ClientHandler do
end
end

defp authenticate_exchange(:auth_query_md5, client_hash, server_hash, salt) do
if "md5" <> H.md5([server_hash, salt]) == client_hash do
{:ok, nil}
else
{:error, "Wrong password"}
end
end

@spec db_checkout(:on_connect | :on_query, map()) :: pid() | nil
defp db_checkout(_, %{mode: :session, db_pid: db_pid}) when is_pid(db_pid) do
db_pid
Expand Down Expand Up @@ -579,11 +617,8 @@ defmodule Supavisor.ClientHandler do
case Cachex.fetch(Supavisor.Cache, cache_key, fn _key ->
{:commit, {:cached, get_secrets(info, db_user)}, ttl: 15_000}
end) do
{_, {:cached, value}} ->
value

{_, {:cached, value}, _} ->
value
{_, {:cached, value}} -> value
{_, {:cached, value}, _} -> value
end
end

Expand Down Expand Up @@ -615,7 +650,8 @@ defmodule Supavisor.ClientHandler do
resp =
case H.get_user_secret(conn, tenant.auth_query, db_user) do
{:ok, secret} ->
{:ok, {:auth_query, fn -> Map.put(secret, :alias, user.db_user_alias) end}}
t = if secret.digest == :md5, do: :auth_query_md5, else: :auth_query
{:ok, {t, fn -> Map.put(secret, :alias, user.db_user_alias) end}}

{:error, reason} ->
{:error, reason}
Expand Down
15 changes: 8 additions & 7 deletions lib/supavisor/db_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,18 @@ defmodule Supavisor.DbHandler do
acc

%{payload: {:authentication_md5_password, salt}}, {ps, _} ->
password = data.auth.password
user = data.auth.user
Logger.debug("dec_pkt, #{inspect(dec_pkt, pretty: true)}")

digest = [password.(), user] |> :erlang.md5() |> Base.encode16(case: :lower)
digest = [digest, salt] |> :erlang.md5() |> Base.encode16(case: :lower)
payload = ["md5", digest, 0]
digest =
if data.auth.method == :password do
H.md5([data.auth.password.(), data.auth.user])
else
data.auth.secrets.().secret
end

payload = ["md5", H.md5([digest, salt]), 0]
bin = [?p, <<IO.iodata_length(payload) + 4::signed-32>>, payload]

:ok = sock_send(data.sock, bin)

{ps, :authentication_md5}

%{tag: :error_response, payload: error}, _ ->
Expand Down
11 changes: 11 additions & 0 deletions lib/supavisor/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ defmodule Supavisor.Helpers do
end
end

def parse_secret("md5" <> secret, user) do
{:ok, %{digest: :md5, secret: secret, user: user}}
end

def parse_postgres_secret(_), do: {:error, "Digest not supported"}

## Internal functions
Expand Down Expand Up @@ -303,4 +307,11 @@ defmodule Supavisor.Helpers do
def parse_server_first(message, nonce) do
:pgo_scram.parse_server_first(message, nonce) |> Map.new()
end

@spec md5([String.t()]) :: String.t()
def md5(strings) do
strings
|> :erlang.md5()
|> Base.encode16(case: :lower)
end
end
22 changes: 17 additions & 5 deletions lib/supavisor/protocol/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Supavisor.Protocol.Server do
@authentication_ok <<?R, 8::32, 0::32>>
@ready_for_query <<?Z, 5::32, ?I>>
@ssl_request <<8::32, 1234::16, 5679::16>>
@auth_request <<?R, 23::32, 10::32, "SCRAM-SHA-256", 0, 0>>
@scram_request <<?R, 23::32, 10::32, "SCRAM-SHA-256", 0, 0>>

defmodule Pkt do
@moduledoc "Representing a packet structure with tag, length, and payload fields."
Expand Down Expand Up @@ -185,6 +185,13 @@ defmodule Supavisor.Protocol.Server do
end
end

def decode_payload(:password_message, "md5" <> _ = bin) do
case String.split(bin, <<0>>) do
[digest, ""] -> {:md5, digest}
_ -> :undefined
end
end

def decode_payload(:password_message, bin) do
case kv_to_map(bin) do
{:ok, map} -> {:first_msg_response, map}
Expand Down Expand Up @@ -258,12 +265,17 @@ defmodule Supavisor.Protocol.Server do
end
end

@spec auth_request() :: iodata()
def auth_request() do
@auth_request
@spec scram_request() :: iodata
def scram_request() do
@scram_request
end

@spec md5_request(<<_::32>>) :: iodata
def md5_request(salt) do
[<<?R, 12::32, 5::32>>, salt]
end

@spec exchange_first_message(binary(), binary() | boolean(), pos_integer()) :: binary()
@spec exchange_first_message(binary, binary | boolean, pos_integer) :: binary
def exchange_first_message(nonce, salt \\ false, iterations \\ 4096) do
secret =
if salt do
Expand Down
3 changes: 2 additions & 1 deletion lib/supavisor/tenant_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ defmodule Supavisor.TenantSupervisor do
name: {:via, Registry, {Supavisor.Registry.Tenants, {:pool, args.id}}},
worker_module: Supavisor.DbHandler,
size: size,
max_overflow: overflow
max_overflow: overflow,
strategy: args.default_pool_strategy
]

children = [
Expand Down
4 changes: 3 additions & 1 deletion lib/supavisor/tenants/tenant.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ defmodule Supavisor.Tenants.Tenant do
field(:sni_hostname, :string)
field(:default_max_clients, :integer, default: 1000)
field(:client_idle_timeout, :integer, default: 0)
field(:default_pool_strategy, Ecto.Enum, values: [:fifo, :lifo], default: :fifo)

has_many(:users, User,
foreign_key: :tenant_external_id,
Expand Down Expand Up @@ -57,7 +58,8 @@ defmodule Supavisor.Tenants.Tenant do
:default_pool_size,
:sni_hostname,
:default_max_clients,
:client_idle_timeout
:client_idle_timeout,
:default_pool_strategy
])
|> check_constraint(:upstream_ssl, name: :upstream_constraints, prefix: "_supavisor")
|> check_constraint(:upstream_verify, name: :upstream_constraints, prefix: "_supavisor")
Expand Down
1 change: 1 addition & 0 deletions lib/supavisor_web/views/tenant_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ defmodule SupavisorWeb.TenantView do
sni_hostname: tenant.sni_hostname,
default_max_clients: tenant.default_max_clients,
client_idle_timeout: tenant.client_idle_timeout,
default_pool_strategy: tenant.default_pool_strategy,
users: render_many(tenant.users, UserView, "user.json")
}
end
Expand Down
Loading

0 comments on commit 03c012a

Please sign in to comment.