Packages

phoenix_kit

2.46.0
2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules storage bucket_credentials.ex
Raw

lib/modules/storage/bucket_credentials.ex

defmodule PhoenixKit.Modules.Storage.BucketCredentials do
@moduledoc """
Moves a bucket's cloud keys into the Integrations system.
A bucket used to carry its own `access_key_id` / `secret_access_key`. Keys now
belong to an `object_storage` connection (**Settings → Integrations**), which
buckets reference by `integration_uuid`: one place to rotate or revoke a key,
one encrypted copy, and the same connection can serve several buckets.
A bucket that still carries its own keys is **legacy** and keeps working for
as long as it is left alone: `Providers.S3.resolve_credentials/1` reads either
source. Nothing here runs by itself. An operator moves a bucket, or all of
them, on purpose, and `move_to_integration/2` refuses to clear a key it could
not read back through the new connection.
The bucket keeps its own `region` and `endpoint`. They are not secrets, and
reading them from the connection would put a settings lookup behind every
public URL a file grid builds.
"""
alias PhoenixKit.Integrations
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.Bucket
alias PhoenixKit.Modules.Storage.Providers.S3
@provider "object_storage"
@type reason ::
:not_legacy
| :unreadable_credentials
| :credentials_mismatch
| Ecto.Changeset.t()
| term()
@doc "The Integrations provider key a bucket's keys move into."
@spec provider_key() :: String.t()
def provider_key, do: @provider
@doc """
Whether `bucket` is a cloud bucket that still carries its own keys, with no
connection set.
"""
@spec legacy?(Bucket.t()) :: boolean()
def legacy?(%Bucket{} = bucket) do
Bucket.cloud?(bucket) and not present?(bucket.integration_uuid) and
(present?(bucket.access_key_id) or present?(bucket.secret_access_key))
end
@doc "Every bucket that still carries its own keys."
@spec legacy_buckets() :: [Bucket.t()]
def legacy_buckets, do: Enum.filter(Storage.list_buckets(), &legacy?/1)
@doc """
Moves one bucket's keys into a system `object_storage` connection.
A connection that already holds the same key pair is reused, so buckets in
one account share a single connection. Otherwise one is created, named after
the bucket. The bucket's own keys are cleared only after the connection is
read back and returns exactly the keys the bucket had; any failure rolls the
whole move back, the new connection included.
Options: `:actor_uuid` (recorded on the connection's activity entries).
"""
@spec move_to_integration(Bucket.t(), keyword()) :: {:ok, Bucket.t()} | {:error, reason()}
def move_to_integration(%Bucket{} = bucket, opts \\ []) do
actor_uuid = Keyword.get(opts, :actor_uuid)
repo = PhoenixKit.RepoHelper.repo()
repo.transaction(fn ->
with :ok <- ensure_legacy(bucket),
{:ok, keys} <- read_keys(bucket),
{:ok, uuid} <- find_or_create_connection(bucket, keys, actor_uuid),
:ok <- verify_round_trip(bucket, uuid, keys),
{:ok, moved} <- clear_keys(bucket, uuid) do
moved
else
{:error, reason} -> repo.rollback(reason)
end
end)
end
@doc """
Moves every legacy bucket. Returns `[{bucket, {:ok, moved} | {:error, reason}}]`,
one entry per bucket that was tried; a bucket that fails does not stop the rest.
"""
@spec move_all_to_integrations(keyword()) :: [
{Bucket.t(), {:ok, Bucket.t()} | {:error, reason()}}
]
def move_all_to_integrations(opts \\ []) do
Enum.map(legacy_buckets(), &{&1, move_to_integration(&1, opts)})
end
defp ensure_legacy(bucket), do: if(legacy?(bucket), do: :ok, else: {:error, :not_legacy})
# The plaintext pair, from the one place that produces it. A secret that
# cannot be decrypted (a rotated `secret_key_base`) must never be "moved":
# that would replace a key that is merely unreadable now with a blank one.
defp read_keys(bucket) do
case S3.resolve_credentials(bucket) do
{key, secret} when is_binary(key) and key != "" and is_binary(secret) and secret != "" ->
{:ok, {key, secret}}
_ ->
{:error, :unreadable_credentials}
end
end
defp find_or_create_connection(bucket, {key, secret}, actor_uuid) do
existing =
Enum.find(Integrations.list_connections(@provider), fn %{data: data} ->
data["access_key"] == key and data["secret_key"] == secret
end)
case existing do
%{uuid: uuid} -> {:ok, uuid}
nil -> create_connection(bucket, {key, secret}, actor_uuid)
end
end
defp create_connection(bucket, {key, secret}, actor_uuid) do
attrs =
Map.reject(
%{
"access_key" => key,
"secret_key" => secret,
"region" => bucket.region,
"endpoint" => bucket.endpoint
},
fn {_field, value} -> not present?(value) end
)
with {:ok, %{uuid: uuid}} <- Integrations.add_connection(@provider, bucket.name, actor_uuid),
{:ok, _saved} <- Integrations.save_setup(uuid, attrs, actor_uuid) do
{:ok, uuid}
end
end
# Reads the keys back through the connection the bucket is about to point at.
defp verify_round_trip(bucket, uuid, keys) do
through_connection = %{
bucket
| integration_uuid: uuid,
access_key_id: nil,
secret_access_key: nil
}
if S3.resolve_credentials(through_connection) == keys,
do: :ok,
else: {:error, :credentials_mismatch}
end
defp clear_keys(bucket, uuid) do
Storage.update_bucket(bucket, %{
integration_uuid: uuid,
access_key_id: nil,
secret_access_key: nil
})
end
defp present?(nil), do: false
defp present?(""), do: false
defp present?(_value), do: true
end