diff --git a/elixir/apps/domain/lib/domain/application.ex b/elixir/apps/domain/lib/domain/application.ex index a0d5d7c991..5eb814587f 100644 --- a/elixir/apps/domain/lib/domain/application.ex +++ b/elixir/apps/domain/lib/domain/application.ex @@ -16,6 +16,7 @@ defmodule Domain.Application do [ # Core services Domain.Repo, + Domain.Vault, Domain.PubSub, # Infrastructure services diff --git a/elixir/apps/domain/lib/domain/auth/adapter.ex b/elixir/apps/domain/lib/domain/auth/adapter.ex index 42721a74f2..bdd078e4d0 100644 --- a/elixir/apps/domain/lib/domain/auth/adapter.ex +++ b/elixir/apps/domain/lib/domain/auth/adapter.ex @@ -30,6 +30,11 @@ defmodule Domain.Auth.Adapter do """ @callback capabilities() :: [capability()] + @doc """ + Loads the polymorphic provider data and puts it back to the provider struct. + """ + @callback load(%Provider{}) :: %Provider{} + @doc """ Applies provider-specific validations for the Identity changeset before it's created. """ diff --git a/elixir/apps/domain/lib/domain/auth/adapter/directory_sync.ex b/elixir/apps/domain/lib/domain/auth/adapter/directory_sync.ex index 69de8e8091..30431cb193 100644 --- a/elixir/apps/domain/lib/domain/auth/adapter/directory_sync.ex +++ b/elixir/apps/domain/lib/domain/auth/adapter/directory_sync.ex @@ -370,7 +370,7 @@ defmodule Domain.Auth.Adapter.DirectorySync do end) |> Enum.reduce({:ok, %{}}, fn {_name, task}, {:error, reason} -> - Task.Supervisor.terminate_child(supervisor, task.pid) + Task.ignore(task) {:error, reason} {name, task}, {:ok, acc} -> diff --git a/elixir/apps/domain/lib/domain/auth/adapters/email.ex b/elixir/apps/domain/lib/domain/auth/adapters/email.ex index 4532229e9b..97ca1c8315 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/email.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/email.ex @@ -114,4 +114,7 @@ defmodule Domain.Auth.Adapters.Email do {:error, :invalid_or_expired_token} -> {:error, :invalid_secret} end end + + @impl true + def load(provider), do: provider end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex index 116811eaae..314853ee07 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex @@ -1,9 +1,11 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace do use Supervisor + alias Domain.Repo alias Domain.Actors alias Domain.Auth.{Provider, Adapter} alias Domain.Auth.Adapters.OpenIDConnect alias Domain.Auth.Adapters.GoogleWorkspace + alias Domain.Auth.Adapters.GoogleWorkspace.{IdentityState, ProviderConfig, ProviderState} require Logger @behaviour Adapter @@ -40,6 +42,12 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace do |> Domain.Repo.Changeset.trim_change(:provider_identifier) |> Domain.Repo.Changeset.copy_change(:provider_virtual_state, :provider_state) |> Ecto.Changeset.put_change(:provider_virtual_state, %{}) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:provider_state, + required: true, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(IdentityState, current_attrs, :json) + end + ) end @impl true @@ -48,8 +56,13 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace do |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_config, required: true, with: fn current_attrs, attrs -> - Ecto.embedded_load(GoogleWorkspace.Settings, current_attrs, :json) - |> GoogleWorkspace.Settings.Changeset.changeset(attrs) + Ecto.embedded_load(ProviderConfig, current_attrs, :json) + |> ProviderConfig.Changeset.changeset(attrs) + end + ) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_state, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(ProviderState, current_attrs, :json) end ) end @@ -66,19 +79,32 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace do @impl true def sign_out(provider, identity, redirect_url) do + provider = load(provider) OpenIDConnect.sign_out(provider, identity, redirect_url) end @impl true def verify_and_update_identity(%Provider{} = provider, payload) do + provider = load(provider) OpenIDConnect.verify_and_update_identity(provider, payload) end def verify_and_upsert_identity(%Actors.Actor{} = actor, %Provider{} = provider, payload) do + provider = load(provider) OpenIDConnect.verify_and_upsert_identity(actor, provider, payload) end def refresh_access_token(%Provider{} = provider) do + provider = load(provider) OpenIDConnect.refresh_access_token(provider) end + + @impl true + def load(%Provider{adapter_config: adapter_config, adapter_state: adapter_state} = provider) do + %{ + provider + | adapter_config: Repo.Changeset.load_polymorphic_embed(ProviderConfig, adapter_config), + adapter_state: Repo.Changeset.load_polymorphic_embed(ProviderState, adapter_state) + } + end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/identity_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/identity_state.ex new file mode 100644 index 0000000000..726d49f0d8 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/identity_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.GoogleWorkspace.IdentityState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs/sync_directory.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs/sync_directory.ex index 7113c8f100..0a5aa6a4bc 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs/sync_directory.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs/sync_directory.ex @@ -23,7 +23,8 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectory do end def gather_provider_data(provider, task_supervisor_pid) do - access_token = provider.adapter_state["access_token"] + provider = GoogleWorkspace.load(provider) + access_token = provider.adapter_state.access_token async_results = DirectorySync.run_async_requests(task_supervisor_pid, diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/settings.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_config.ex similarity index 84% rename from elixir/apps/domain/lib/domain/auth/adapters/google_workspace/settings.ex rename to elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_config.ex index 1f3a438d9b..4a7194cd06 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/settings.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_config.ex @@ -1,4 +1,4 @@ -defmodule Domain.Auth.Adapters.GoogleWorkspace.Settings do +defmodule Domain.Auth.Adapters.GoogleWorkspace.ProviderConfig do use Domain, :schema @scope ~w[ @@ -15,7 +15,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Settings do field :scope, :string, default: Enum.join(@scope, " ") field :response_type, :string, default: "code" field :client_id, :string - field :client_secret, :string + field :client_secret, Domain.Types.EncryptedString field :discovery_document_uri, :string, default: @discovery_document_uri end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_config/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_config/changeset.ex new file mode 100644 index 0000000000..1fd264b3fc --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_config/changeset.ex @@ -0,0 +1,19 @@ +defmodule Domain.Auth.Adapters.GoogleWorkspace.ProviderConfig.Changeset do + use Domain, :changeset + alias Domain.Auth.Adapters.GoogleWorkspace.ProviderConfig + alias Domain.Auth.Adapters.OpenIDConnect + + @fields ~w[scope + response_type + client_id client_secret + discovery_document_uri]a + + def changeset(%ProviderConfig{} = settings, attrs) do + settings + |> cast(attrs, @fields) + |> validate_required(@fields) + |> validate_inclusion(:response_type, ~w[code]) + |> OpenIDConnect.ProviderConfig.Changeset.validate_discovery_document_uri() + |> OpenIDConnect.ProviderConfig.Changeset.validate_scope(:scope, ProviderConfig.scope()) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_state.ex new file mode 100644 index 0000000000..886af4ff5f --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/provider_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.GoogleWorkspace.ProviderState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/settings/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/settings/changeset.ex deleted file mode 100644 index dc6baf4c1f..0000000000 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/settings/changeset.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Domain.Auth.Adapters.GoogleWorkspace.Settings.Changeset do - use Domain, :changeset - alias Domain.Auth.Adapters.GoogleWorkspace.Settings - alias Domain.Auth.Adapters.OpenIDConnect - - @fields ~w[scope - response_type - client_id client_secret - discovery_document_uri]a - - def changeset(%Settings{} = settings, attrs) do - changeset = - settings - |> cast(attrs, @fields) - |> validate_required(@fields) - |> OpenIDConnect.Settings.Changeset.validate_discovery_document_uri() - |> validate_inclusion(:response_type, ~w[code]) - - Enum.reduce(Settings.scope(), changeset, fn scope, changeset -> - validate_format(changeset, :scope, ~r/#{scope}/, message: "must include #{scope} scope") - end) - end -end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra.ex b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra.ex index 542fdb3d9f..818a29b9ed 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra.ex @@ -1,9 +1,11 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra do use Supervisor + alias Domain.Repo alias Domain.Actors alias Domain.Auth.{Provider, Adapter} alias Domain.Auth.Adapters.OpenIDConnect alias Domain.Auth.Adapters.MicrosoftEntra + alias Domain.Auth.Adapters.MicrosoftEntra.{IdentityState, ProviderConfig, ProviderState} require Logger @behaviour Adapter @@ -40,6 +42,12 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra do |> Domain.Repo.Changeset.trim_change(:provider_identifier) |> Domain.Repo.Changeset.copy_change(:provider_virtual_state, :provider_state) |> Ecto.Changeset.put_change(:provider_virtual_state, %{}) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:provider_state, + required: true, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(IdentityState, current_attrs, :json) + end + ) end @impl true @@ -48,8 +56,13 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra do |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_config, required: true, with: fn current_attrs, attrs -> - Ecto.embedded_load(MicrosoftEntra.Settings, current_attrs, :json) - |> MicrosoftEntra.Settings.Changeset.changeset(attrs) + Ecto.embedded_load(ProviderConfig, current_attrs, :json) + |> ProviderConfig.Changeset.changeset(attrs) + end + ) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_state, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(ProviderState, current_attrs, :json) end ) end @@ -66,19 +79,32 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra do @impl true def sign_out(provider, identity, redirect_url) do + provider = load(provider) OpenIDConnect.sign_out(provider, identity, redirect_url) end @impl true def verify_and_update_identity(%Provider{} = provider, payload) do + provider = load(provider) OpenIDConnect.verify_and_update_identity(provider, payload, "oid") end def verify_and_upsert_identity(%Actors.Actor{} = actor, %Provider{} = provider, payload) do + provider = load(provider) OpenIDConnect.verify_and_upsert_identity(actor, provider, payload, "oid") end def refresh_access_token(%Provider{} = provider) do + provider = load(provider) OpenIDConnect.refresh_access_token(provider) end + + @impl true + def load(%Provider{adapter_config: adapter_config, adapter_state: adapter_state} = provider) do + %{ + provider + | adapter_config: Repo.Changeset.load_polymorphic_embed(ProviderConfig, adapter_config), + adapter_state: Repo.Changeset.load_polymorphic_embed(ProviderState, adapter_state) + } + end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/identity_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/identity_state.ex new file mode 100644 index 0000000000..712d93f2e7 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/identity_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.MicrosoftEntra.IdentityState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/jobs/sync_directory.ex b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/jobs/sync_directory.ex index 07a89ffe40..7db3c23b07 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/jobs/sync_directory.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/jobs/sync_directory.ex @@ -23,7 +23,8 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.SyncDirectory do end def gather_provider_data(provider, task_supervisor_pid) do - access_token = provider.adapter_state["access_token"] + provider = MicrosoftEntra.load(provider) + access_token = provider.adapter_state.access_token async_results = DirectorySync.run_async_requests(task_supervisor_pid, diff --git a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/settings.ex b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_config.ex similarity index 77% rename from elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/settings.ex rename to elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_config.ex index 9bfc77b4f5..1bb06e00de 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/settings.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_config.ex @@ -1,4 +1,4 @@ -defmodule Domain.Auth.Adapters.MicrosoftEntra.Settings do +defmodule Domain.Auth.Adapters.MicrosoftEntra.ProviderConfig do use Domain, :schema @scope ~w[ @@ -15,7 +15,7 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Settings do field :scope, :string, default: Enum.join(@scope, " ") field :response_type, :string, default: "code" field :client_id, :string - field :client_secret, :string + field :client_secret, Domain.Types.EncryptedString field :discovery_document_uri, :string end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_config/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_config/changeset.ex new file mode 100644 index 0000000000..dc144de528 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_config/changeset.ex @@ -0,0 +1,19 @@ +defmodule Domain.Auth.Adapters.MicrosoftEntra.ProviderConfig.Changeset do + use Domain, :changeset + alias Domain.Auth.Adapters.MicrosoftEntra.ProviderConfig + alias Domain.Auth.Adapters.OpenIDConnect + + @fields ~w[scope + response_type + client_id client_secret + discovery_document_uri]a + + def changeset(%ProviderConfig{} = settings, attrs) do + settings + |> cast(attrs, @fields) + |> validate_required(@fields) + |> validate_inclusion(:response_type, ~w[code]) + |> OpenIDConnect.ProviderConfig.Changeset.validate_discovery_document_uri() + |> OpenIDConnect.ProviderConfig.Changeset.validate_scope(:scope, ProviderConfig.scope()) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_state.ex new file mode 100644 index 0000000000..b26307245f --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/provider_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.MicrosoftEntra.ProviderState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/settings/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/settings/changeset.ex deleted file mode 100644 index e8d3779ce3..0000000000 --- a/elixir/apps/domain/lib/domain/auth/adapters/microsoft_entra/settings/changeset.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Domain.Auth.Adapters.MicrosoftEntra.Settings.Changeset do - use Domain, :changeset - alias Domain.Auth.Adapters.MicrosoftEntra.Settings - alias Domain.Auth.Adapters.OpenIDConnect - - @fields ~w[scope - response_type - client_id client_secret - discovery_document_uri]a - - def changeset(%Settings{} = settings, attrs) do - changeset = - settings - |> cast(attrs, @fields) - |> validate_required(@fields) - |> OpenIDConnect.Settings.Changeset.validate_discovery_document_uri() - |> validate_inclusion(:response_type, ~w[code]) - - Enum.reduce(Settings.scope(), changeset, fn scope, changeset -> - validate_format(changeset, :scope, ~r/#{scope}/, message: "must include #{scope} scope") - end) - end -end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta.ex index c6e449d91f..282ea93341 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/okta.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta.ex @@ -1,9 +1,11 @@ defmodule Domain.Auth.Adapters.Okta do use Supervisor + alias Domain.Repo alias Domain.Actors alias Domain.Auth.{Provider, Adapter} alias Domain.Auth.Adapters.OpenIDConnect alias Domain.Auth.Adapters.Okta + alias Domain.Auth.Adapters.Okta.{IdentityState, ProviderConfig, ProviderState} require Logger @behaviour Adapter @@ -40,6 +42,12 @@ defmodule Domain.Auth.Adapters.Okta do |> Domain.Repo.Changeset.trim_change(:provider_identifier) |> Domain.Repo.Changeset.copy_change(:provider_virtual_state, :provider_state) |> Ecto.Changeset.put_change(:provider_virtual_state, %{}) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:provider_state, + required: true, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(IdentityState, current_attrs, :json) + end + ) end @impl true @@ -48,8 +56,13 @@ defmodule Domain.Auth.Adapters.Okta do |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_config, required: true, with: fn current_attrs, attrs -> - Ecto.embedded_load(Okta.Settings, current_attrs, :json) - |> Okta.Settings.Changeset.changeset(attrs) + Ecto.embedded_load(ProviderConfig, current_attrs, :json) + |> ProviderConfig.Changeset.changeset(attrs) + end + ) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_state, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(ProviderState, current_attrs, :json) end ) end @@ -66,19 +79,32 @@ defmodule Domain.Auth.Adapters.Okta do @impl true def sign_out(provider, identity, redirect_url) do + provider = load(provider) OpenIDConnect.sign_out(provider, identity, redirect_url) end @impl true def verify_and_update_identity(%Provider{} = provider, payload) do + provider = load(provider) OpenIDConnect.verify_and_update_identity(provider, payload) end def verify_and_upsert_identity(%Actors.Actor{} = actor, %Provider{} = provider, payload) do + provider = load(provider) OpenIDConnect.verify_and_upsert_identity(actor, provider, payload) end def refresh_access_token(%Provider{} = provider) do + provider = load(provider) OpenIDConnect.refresh_access_token(provider) end + + @impl true + def load(%Provider{adapter_config: adapter_config, adapter_state: adapter_state} = provider) do + %{ + provider + | adapter_config: Repo.Changeset.load_polymorphic_embed(ProviderConfig, adapter_config), + adapter_state: Repo.Changeset.load_polymorphic_embed(ProviderState, adapter_state) + } + end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/identity_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/identity_state.ex new file mode 100644 index 0000000000..a565a0260a --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/identity_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.Okta.IdentityState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex index 391aac148f..87551ed655 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex @@ -23,8 +23,9 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectory do end def gather_provider_data(provider, task_supervisor_pid) do - endpoint = provider.adapter_config["api_base_url"] - access_token = provider.adapter_state["access_token"] + provider = Okta.load(provider) + endpoint = provider.adapter_config.api_base_url + access_token = provider.adapter_state.access_token async_results = DirectorySync.run_async_requests(task_supervisor_pid, diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/settings.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_config.ex similarity index 80% rename from elixir/apps/domain/lib/domain/auth/adapters/okta/settings.ex rename to elixir/apps/domain/lib/domain/auth/adapters/okta/provider_config.ex index f8bd716aa3..49b08b4fbc 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/okta/settings.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_config.ex @@ -1,4 +1,4 @@ -defmodule Domain.Auth.Adapters.Okta.Settings do +defmodule Domain.Auth.Adapters.Okta.ProviderConfig do use Domain, :schema @scope ~w[ @@ -13,7 +13,7 @@ defmodule Domain.Auth.Adapters.Okta.Settings do field :scope, :string, default: Enum.join(@scope, " ") field :response_type, :string, default: "code" field :client_id, :string - field :client_secret, :string + field :client_secret, Domain.Types.EncryptedString field :discovery_document_uri, :string field :okta_account_domain, :string field :api_base_url, :string diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_config/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_config/changeset.ex new file mode 100644 index 0000000000..778e135c91 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_config/changeset.ex @@ -0,0 +1,21 @@ +defmodule Domain.Auth.Adapters.Okta.ProviderConfig.Changeset do + use Domain, :changeset + alias Domain.Auth.Adapters.Okta.ProviderConfig + alias Domain.Auth.Adapters.OpenIDConnect + + @fields ~w[scope + response_type + client_id client_secret + discovery_document_uri + okta_account_domain + api_base_url]a + + def changeset(%ProviderConfig{} = settings, attrs) do + settings + |> cast(attrs, @fields) + |> validate_required(@fields) + |> validate_inclusion(:response_type, ~w[code]) + |> OpenIDConnect.ProviderConfig.Changeset.validate_discovery_document_uri() + |> OpenIDConnect.ProviderConfig.Changeset.validate_scope(:scope, ProviderConfig.scope()) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_state.ex new file mode 100644 index 0000000000..341562eb5e --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/provider_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.Okta.ProviderState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/settings/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/settings/changeset.ex deleted file mode 100644 index 43dcb71e6b..0000000000 --- a/elixir/apps/domain/lib/domain/auth/adapters/okta/settings/changeset.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Domain.Auth.Adapters.Okta.Settings.Changeset do - use Domain, :changeset - alias Domain.Auth.Adapters.Okta.Settings - alias Domain.Auth.Adapters.OpenIDConnect - - @fields ~w[scope - response_type - client_id client_secret - discovery_document_uri - okta_account_domain - api_base_url]a - - def changeset(%Settings{} = settings, attrs) do - changeset = - settings - |> cast(attrs, @fields) - |> validate_required(@fields) - |> OpenIDConnect.Settings.Changeset.validate_discovery_document_uri() - |> validate_inclusion(:response_type, ~w[code]) - - Enum.reduce(Settings.scope(), changeset, fn scope, changeset -> - validate_format(changeset, :scope, ~r/#{scope}/, message: "must include #{scope} scope") - end) - end -end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex index 9d5741fb48..3e4d711a09 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex @@ -3,7 +3,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do alias Domain.Repo alias Domain.Actors alias Domain.Auth.{Identity, Provider, Adapter} - alias Domain.Auth.Adapters.OpenIDConnect.{Settings, State, PKCE} + alias Domain.Auth.Adapters.OpenIDConnect.{ProviderConfig, ProviderState, IdentityState, PKCE} require Logger @behaviour Adapter @@ -35,6 +35,11 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do |> Domain.Repo.Changeset.trim_change(:provider_identifier) |> Domain.Repo.Changeset.copy_change(:provider_virtual_state, :provider_state) |> Ecto.Changeset.put_change(:provider_virtual_state, %{}) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:provider_state, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(IdentityState, current_attrs, :json) + end + ) end @impl true @@ -43,8 +48,13 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_config, required: true, with: fn current_attrs, attrs -> - Ecto.embedded_load(Settings, current_attrs, :json) - |> Settings.Changeset.changeset(attrs) + Ecto.embedded_load(ProviderConfig, current_attrs, :json) + |> ProviderConfig.Changeset.changeset(attrs) + end + ) + |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_state, + with: fn current_attrs, _attrs -> + Ecto.embedded_load(ProviderState, current_attrs, :json) end ) end @@ -61,10 +71,10 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do def authorization_uri(%Provider{} = provider, redirect_uri, params \\ %{}) when is_binary(redirect_uri) do - config = config_for_provider(provider) + provider = load(provider) verifier = PKCE.code_verifier() - state = State.new() + state = authorization_uri_state() params = Map.merge( @@ -77,13 +87,19 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do params ) - with {:ok, uri} <- OpenIDConnect.authorization_uri(config, redirect_uri, params) do + with {:ok, uri} <- + OpenIDConnect.authorization_uri(provider.adapter_config, redirect_uri, params) do {:ok, uri, {state, verifier}} end end + @doc false + def authorization_uri_state do + Domain.Crypto.random_token(32) + end + def ensure_states_equal(state1, state2) do - if State.equal?(state1, state2) do + if Plug.Crypto.secure_compare(state1, state2) do :ok else {:error, :invalid_state} @@ -92,9 +108,9 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do @impl true def sign_out(%Provider{} = provider, %Identity{} = identity, redirect_url) do - config = config_for_provider(provider) + provider = load(provider) - case OpenIDConnect.end_session_uri(config, %{ + case OpenIDConnect.end_session_uri(provider.adapter_config, %{ id_token_hint: identity.provider_state["id_token"], post_logout_redirect_uri: redirect_url }) do @@ -112,6 +128,8 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do {redirect_uri, code_verifier, code}, identifier_claim \\ "sub" ) do + provider = load(provider) + token_params = %{ grant_type: "authorization_code", redirect_uri: redirect_uri, @@ -119,25 +137,25 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do code_verifier: code_verifier } - with {:ok, provider_identifier, identity_state} <- + with {:ok, provider_identifier, identity_provider_state} <- fetch_state(provider, token_params, identifier_claim) do Identity.Query.not_disabled() |> Identity.Query.by_provider_id(provider.id) |> maybe_by_provider_claims( provider, provider_identifier, - identity_state + identity_provider_state ) |> Repo.fetch_and_update(Identity.Query, with: fn identity -> - Identity.Changeset.update_identity_provider_state(identity, identity_state) + Identity.Changeset.update_identity_provider_state(identity, identity_provider_state) # if an email was used in provider identifier and it's replaced by sub claim # later, we want to use the ID from sub claim as provider_identifier |> Ecto.Changeset.put_change(:provider_identifier, provider_identifier) end ) |> case do - {:ok, identity} -> {:ok, identity, identity_state["expires_at"]} + {:ok, identity} -> {:ok, identity, identity_provider_state["expires_at"]} {:error, reason} -> {:error, reason} end else @@ -151,13 +169,13 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do queryable, provider, provider_identifier, - identity_state + identity_provider_state ) do if provider.provisioner == :manual do Identity.Query.by_provider_claims( queryable, provider_identifier, - identity_state["claims"]["email"] || identity_state["userinfo"]["email"] + identity_provider_state["claims"]["email"] || identity_provider_state["userinfo"]["email"] ) else Identity.Query.by_provider_identifier(queryable, provider_identifier) @@ -170,6 +188,8 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do {redirect_uri, code_verifier, code}, identifier_claim \\ "sub" ) do + provider = load(provider) + token_params = %{ grant_type: "authorization_code", redirect_uri: redirect_uri, @@ -177,33 +197,40 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do code_verifier: code_verifier } - with {:ok, provider_identifier, identity_state} <- + with {:ok, provider_identifier, identity_provider_state} <- fetch_state(provider, token_params, identifier_claim) do Domain.Auth.upsert_identity(actor, provider, %{ provider_identifier: provider_identifier, - provider_virtual_state: identity_state + provider_virtual_state: identity_provider_state }) end end def refresh_access_token(%Provider{} = provider) do + provider = load(provider) + token_params = %{ grant_type: "refresh_token", - refresh_token: provider.adapter_state["refresh_token"] + refresh_token: provider.adapter_state.refresh_token } - with {:ok, _provider_identifier, adapter_state} <- + with {:ok, _provider_identifier, provider_adapter_state} <- fetch_state(provider, token_params) do Provider.Query.not_deleted() |> Provider.Query.by_id(provider.id) |> Repo.fetch_and_update(Provider.Query, with: fn provider -> adapter_state_updates = - Map.take(adapter_state, ["expires_at", "access_token", "userinfo", "claims"]) - - adapter_state = Map.merge(provider.adapter_state, adapter_state_updates) - - Provider.Changeset.update(provider, %{adapter_state: adapter_state}) + Map.take(provider_adapter_state, [ + "expires_at", + "access_token", + "userinfo", + "claims" + ]) + + # this state is encrypted at the schema level + provider_adapter_state = Map.merge(provider.adapter_state, adapter_state_updates) + Provider.Changeset.update(provider, %{adapter_state: provider_adapter_state}) end ) else @@ -239,11 +266,10 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do end defp fetch_state(%Provider{} = provider, token_params, identifier_claim \\ "sub") do - config = config_for_provider(provider) - - with {:ok, tokens} <- OpenIDConnect.fetch_tokens(config, token_params), - {:ok, claims} <- OpenIDConnect.verify(config, tokens["id_token"]), - {:ok, userinfo} <- OpenIDConnect.fetch_userinfo(config, tokens["access_token"]) do + with {:ok, tokens} <- OpenIDConnect.fetch_tokens(provider.adapter_config, token_params), + {:ok, claims} <- OpenIDConnect.verify(provider.adapter_config, tokens["id_token"]), + {:ok, userinfo} <- + OpenIDConnect.fetch_userinfo(provider.adapter_config, tokens["access_token"]) do expires_at = cond do not is_nil(tokens["expires_in"]) -> @@ -292,8 +318,12 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do end end - defp config_for_provider(%Provider{} = provider) do - Ecto.embedded_load(Settings, provider.adapter_config, :json) - |> Map.from_struct() + @impl true + def load(%Provider{adapter_config: adapter_config, adapter_state: adapter_state} = provider) do + %{ + provider + | adapter_config: Repo.Changeset.load_polymorphic_embed(ProviderConfig, adapter_config), + adapter_state: Repo.Changeset.load_polymorphic_embed(ProviderState, adapter_state) + } end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/identity_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/identity_state.ex new file mode 100644 index 0000000000..de15e5ba2b --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/identity_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.OpenIDConnect.IdentityState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_config.ex similarity index 68% rename from elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings.ex rename to elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_config.ex index 95da67bbee..f9c852f7e6 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_config.ex @@ -1,4 +1,4 @@ -defmodule Domain.Auth.Adapters.OpenIDConnect.Settings do +defmodule Domain.Auth.Adapters.OpenIDConnect.ProviderConfig do use Domain, :schema @primary_key false @@ -6,7 +6,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnect.Settings do field :scope, :string, default: "openid email profile" field :response_type, :string, default: "code" field :client_id, :string - field :client_secret, :string + field :client_secret, Domain.Types.EncryptedString field :discovery_document_uri, :string end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_config/changeset.ex similarity index 83% rename from elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings/changeset.ex rename to elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_config/changeset.ex index 2f9a842b41..c6716b8804 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_config/changeset.ex @@ -1,4 +1,4 @@ -defmodule Domain.Auth.Adapters.OpenIDConnect.Settings.Changeset do +defmodule Domain.Auth.Adapters.OpenIDConnect.ProviderConfig.Changeset do use Domain, :changeset @fields ~w[scope @@ -15,6 +15,12 @@ defmodule Domain.Auth.Adapters.OpenIDConnect.Settings.Changeset do |> validate_format(:scope, ~r/openid/, message: "must include openid scope") end + def validate_scope(changeset, field, scopes) do + Enum.reduce(scopes, changeset, fn scope, changeset -> + validate_format(changeset, field, ~r/#{scope}/, message: "must include #{scope} scope") + end) + end + def validate_discovery_document_uri(changeset) do validate_change(changeset, :discovery_document_uri, fn :discovery_document_uri, value -> with {:ok, %URI{scheme: scheme, host: host}} diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_state.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_state.ex new file mode 100644 index 0000000000..99eeb1a136 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/provider_state.ex @@ -0,0 +1,14 @@ +defmodule Domain.Auth.Adapters.OpenIDConnect.ProviderState do + use Domain, :schema + + @primary_key false + embedded_schema do + field :access_token, Domain.Types.EncryptedString + field :refresh_token, Domain.Types.EncryptedString + + field :userinfo, :map + field :claims, :map + + field :expires_at, :utc_datetime_usec + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/state.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/state.ex deleted file mode 100644 index 983cb73094..0000000000 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/state.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Domain.Auth.Adapters.OpenIDConnect.State do - def new do - Domain.Crypto.random_token(32) - end - - def equal?(state1, state2) do - Plug.Crypto.secure_compare(state1, state2) - end -end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex b/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex index b557625191..db7ee3215e 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex @@ -6,7 +6,7 @@ defmodule Domain.Auth.Adapters.UserPass do use Supervisor alias Domain.Repo alias Domain.Auth.{Identity, Provider, Adapter, Context} - alias Domain.Auth.Adapters.UserPass.Password + alias Domain.Auth.Adapters.UserPass.IdentityState @behaviour Adapter @behaviour Adapter.Local @@ -42,8 +42,8 @@ defmodule Domain.Auth.Adapters.UserPass do data = Map.get(changeset.data, :provider_virtual_state) || %{} attrs = Ecto.Changeset.get_change(changeset, :provider_virtual_state) || %{} - Ecto.embedded_load(Password, data, :json) - |> Password.Changeset.changeset(attrs) + Ecto.embedded_load(IdentityState, data, :json) + |> IdentityState.Changeset.changeset(attrs) |> case do %{valid?: false} = nested_changeset -> {changeset, _original_type} = @@ -116,4 +116,7 @@ defmodule Domain.Auth.Adapters.UserPass do {:error, reason} end end + + @impl true + def load(provider), do: provider end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/userpass/password.ex b/elixir/apps/domain/lib/domain/auth/adapters/userpass/identity_state.ex similarity index 64% rename from elixir/apps/domain/lib/domain/auth/adapters/userpass/password.ex rename to elixir/apps/domain/lib/domain/auth/adapters/userpass/identity_state.ex index f36679bfc8..e2122cdfef 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/userpass/password.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/userpass/identity_state.ex @@ -1,10 +1,10 @@ -defmodule Domain.Auth.Adapters.UserPass.Password do +defmodule Domain.Auth.Adapters.UserPass.IdentityState do use Domain, :schema @primary_key false embedded_schema do field :password, :string, virtual: true, redact: true - field :password_hash, :string + field :password_hash, Domain.Types.EncryptedString field :password_confirmation, :string, virtual: true, redact: true end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/userpass/password/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/userpass/identity_state/changeset.ex similarity index 85% rename from elixir/apps/domain/lib/domain/auth/adapters/userpass/password/changeset.ex rename to elixir/apps/domain/lib/domain/auth/adapters/userpass/identity_state/changeset.ex index 0da516a14c..f61a589c2a 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/userpass/password/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/userpass/identity_state/changeset.ex @@ -1,12 +1,12 @@ -defmodule Domain.Auth.Adapters.UserPass.Password.Changeset do +defmodule Domain.Auth.Adapters.UserPass.IdentityState.Changeset do use Domain, :changeset - alias Domain.Auth.Adapters.UserPass.Password + alias Domain.Auth.Adapters.UserPass.IdentityState @fields ~w[password password_confirmation]a @min_password_length 12 @max_password_length 72 - def changeset(%Password{} = struct, attrs) do + def changeset(%IdentityState{} = struct, attrs) do struct |> cast(attrs, @fields) |> validate_required(@fields) diff --git a/elixir/apps/domain/lib/domain/auth/provider/query.ex b/elixir/apps/domain/lib/domain/auth/provider/query.ex index 6ac1566a8a..b83581a33e 100644 --- a/elixir/apps/domain/lib/domain/auth/provider/query.ex +++ b/elixir/apps/domain/lib/domain/auth/provider/query.ex @@ -30,6 +30,10 @@ defmodule Domain.Auth.Provider.Query do where(queryable, [providers: providers], providers.adapter not in ^adapters) end + def by_adapter(queryable, {:in, adapters}) do + where(queryable, [providers: providers], providers.adapter in ^adapters) + end + def by_adapter(queryable, adapter) do where(queryable, [providers: providers], providers.adapter == ^adapter) end diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index df86dfc084..aecf226dc4 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -67,7 +67,8 @@ defmodule Domain.Config.Definitions do :database_pool_size, :database_ssl_enabled, :database_ssl_opts, - :database_parameters + :database_parameters, + :database_encryption_key ]}, {"Cloud Platform", [ @@ -292,6 +293,22 @@ defmodule Domain.Config.Definitions do dump: &Dumper.keyword/1 ) + @doc """ + List of base64-encoded encryption keys that are used to encrypt the database. + + First key will be used as default key and rest are retired ones that + can still be used during the key rotation. + + To migrate the encrypted data use `migrate_encryption_keys` boot command or run `Domain.Vault.migrate_keys()` + in the iex shell. + """ + defconfig(:database_encryption_keys, {:array, :string}, + sensitive: true, + changeset: &Domain.Repo.Changeset.validate_base64/2, + dump: fn values -> Enum.map(values, &Base.decode64!/1) end, + default: [] + ) + ############################################## ## Platform ############################################## diff --git a/elixir/apps/domain/lib/domain/repo/changeset.ex b/elixir/apps/domain/lib/domain/repo/changeset.ex index 6932bd129a..e447ea4455 100644 --- a/elixir/apps/domain/lib/domain/repo/changeset.ex +++ b/elixir/apps/domain/lib/domain/repo/changeset.ex @@ -375,6 +375,14 @@ defmodule Domain.Repo.Changeset do # Polymorphic embeds + @doc """ + Loads polymorphic embed `data` into a struct of the given `schema`. + + If the data is already a `schema` struct, it is returned as is. + """ + def load_polymorphic_embed(_schema, %_loaded_schema{} = data), do: data + def load_polymorphic_embed(schema, data), do: Ecto.embedded_load(schema, data, :json) + @doc """ Changes `Ecto.Changeset` struct to convert one of `:map` fields to an embedded schema. @@ -409,7 +417,7 @@ defmodule Domain.Repo.Changeset do required? = Keyword.get(opts, :required, false) # We only support singular polymorphic embeds for now - :map = Map.get(changeset.types, field) + true = Map.get(changeset.types, field) in [:map, Domain.Types.EncryptedMap] if field_invalid?(changeset, field) do changeset @@ -420,9 +428,16 @@ defmodule Domain.Repo.Changeset do if required? and is_nil(changes) and empty?(data) do add_error(changeset, field, "can't be blank", validation: :required) else - %Changeset{} = nested_changeset = on_cast.(data || %{}, changes || %{}) - {changeset, original_type} = inject_embedded_changeset(changeset, field, nested_changeset) - prepare_changes(changeset, &dump(&1, field, original_type)) + case on_cast.(data || %{}, changes || %{}) do + %Changeset{} = nested_changeset -> + {changeset, original_type} = + inject_embedded_changeset(changeset, field, nested_changeset) + + prepare_changes(changeset, &dump(&1, field, original_type)) + + _schema -> + prepare_changes(changeset, &dump(&1, field)) + end end end end @@ -473,6 +488,23 @@ defmodule Domain.Repo.Changeset do put_change(changeset, field, map) end + defp dump(changeset, field) do + changeset + |> get_change(field) + |> case do + %{__meta__: _meta} = schema -> + map = + schema + |> Ecto.embedded_dump(:json) + |> atom_keys_to_string() + + put_change(changeset, field, map) + + _map -> + changeset + end + end + # We dump atoms to strings because if we persist to Postgres and read it, # the map will be returned with string keys, and we want to make sure that # the map handling is unified across the codebase. diff --git a/elixir/apps/domain/lib/domain/types/encrypted_map.ex b/elixir/apps/domain/lib/domain/types/encrypted_map.ex new file mode 100644 index 0000000000..fa143e4285 --- /dev/null +++ b/elixir/apps/domain/lib/domain/types/encrypted_map.ex @@ -0,0 +1,3 @@ +defmodule Domain.Types.EncryptedMap do + use Cloak.Ecto.Map, vault: Domain.Vault +end diff --git a/elixir/apps/domain/lib/domain/types/encrypted_string.ex b/elixir/apps/domain/lib/domain/types/encrypted_string.ex new file mode 100644 index 0000000000..c297102952 --- /dev/null +++ b/elixir/apps/domain/lib/domain/types/encrypted_string.ex @@ -0,0 +1,36 @@ +defmodule Domain.Types.EncryptedString do + @moduledoc """ + A type that encrypts and decrypts string and dumps it to Base64-encoded string. + """ + use Cloak.Ecto.Type, vault: Domain.Vault + + @impl Ecto.Type + def embed_as(_format), do: :dump + + @impl Ecto.Type + def dump(value) do + with {:ok, value} <- cast(value), + value <- before_encrypt(value), + {:ok, value} <- encrypt(value) do + {:ok, Base.encode64(value)} + else + _other -> + :error + end + end + + @impl Ecto.Type + def load(nil) do + {:ok, nil} + end + + def load(value) do + with {:ok, value} <- Base.decode64(value), + {:ok, value} <- decrypt(value) do + {:ok, after_decrypt(value)} + else + _other -> + :error + end + end +end diff --git a/elixir/apps/domain/lib/vault.ex b/elixir/apps/domain/lib/vault.ex new file mode 100644 index 0000000000..325242e798 --- /dev/null +++ b/elixir/apps/domain/lib/vault.ex @@ -0,0 +1,11 @@ +defmodule Domain.Vault do + use Cloak.Vault, otp_app: :domain + + def migrate_keys do + repo = Application.fetch_env!(:domain, :cloak_repo) + + for schema <- Application.get_env(:domain, :cloak_schemas, []) do + Cloak.Ecto.Migrator.migrate(repo, schema) + end + end +end diff --git a/elixir/apps/domain/mix.exs b/elixir/apps/domain/mix.exs index dd450014bc..b7ce12f2f0 100644 --- a/elixir/apps/domain/mix.exs +++ b/elixir/apps/domain/mix.exs @@ -46,6 +46,7 @@ defmodule Domain.MixProject do {:postgrex, "~> 0.16"}, {:decimal, "~> 2.0"}, {:ecto_sql, "~> 3.7"}, + {:cloak_ecto, "~> 1.3"}, # PubSub and Presence {:phoenix, "~> 1.7"}, diff --git a/elixir/apps/domain/priv/repo/migrations/20240426014046_encrypt_provider_and_identity_state.exs b/elixir/apps/domain/priv/repo/migrations/20240426014046_encrypt_provider_and_identity_state.exs new file mode 100644 index 0000000000..a51e5416b2 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20240426014046_encrypt_provider_and_identity_state.exs @@ -0,0 +1,59 @@ +defmodule Domain.Repo.Migrations.EncryptProviderAndIdentityState do + use Ecto.Migration + + def change do + execute(fn -> + Domain.Auth.Provider.Query.not_deleted() + |> Domain.Auth.Provider.Query.by_adapter( + {:in, + [ + :openid_connect, + :google_workspace, + :microsoft_entra, + :okta + ]} + ) + |> repo().all() + |> Enum.each(fn provider -> + adapter_config = + provider.adapter_config + |> encrypt_key!(:client_secret) + + adapter_state = + provider.adapter_state + |> encrypt_key!(:access_token) + |> encrypt_key!(:refresh_token) + + provider + |> Ecto.Changeset.change( + adapter_config: adapter_config, + adapter_state: adapter_state + ) + |> repo().update!() + end) + + Domain.Auth.Identity.Query.all() + |> repo().all() + |> Enum.each(fn identity -> + identity_provider_state = + identity.provider_state + |> encrypt_key!(:access_token) + |> encrypt_key!(:refresh_token) + + identity + |> Ecto.Changeset.change(provider_state: identity_provider_state) + |> repo().update!() + end) + end) + end + + defp encrypt_key!(map, key) do + {_value, map} = + Map.get_and_update(map, key, fn + nil -> :pop + value -> {value, Domain.Vault.encrypt!(value)} + end) + + map + end +end diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/refresh_access_tokens_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/refresh_access_tokens_test.exs index 7bdb1603a9..2ce4b09a4a 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/refresh_access_tokens_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/refresh_access_tokens_test.exs @@ -12,10 +12,10 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.RefreshAccessTokensTest do provider = Domain.Fixture.update!(provider, %{ adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => Fixtures.Auth.encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() |> DateTime.add(15, :minute), - "claims" => "openid email profile offline_access" + "claims" => %{} } }) @@ -54,7 +54,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.RefreshAccessTokensTest do "access_token" => "MY_ACCESS_TOKEN", "claims" => ^claims, "expires_at" => expires_at, - "refresh_token" => "OIDC_REFRESH_TOKEN", + "refresh_token" => refresh_token, "userinfo" => %{ "email" => "ada@example.com", "email_verified" => true, @@ -68,7 +68,27 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.RefreshAccessTokensTest do } } = provider.adapter_state + assert refresh_token + |> Base.decode64!() + |> Domain.Vault.decrypt!() == "OIDC_REFRESH_TOKEN" + assert expires_at + + assert_receive {:request, %{request_path: "/oauth/token"} = conn} + + adapter_config = + Ecto.embedded_load( + Domain.Auth.Adapters.GoogleWorkspace.ProviderConfig, + provider.adapter_config, + :json + ) + + assert conn.body_params == %{ + "client_id" => adapter_config.client_id, + "client_secret" => adapter_config.client_secret, + "grant_type" => "refresh_token", + "refresh_token" => "OIDC_REFRESH_TOKEN" + } end test "does not crash when endpoint it not available", %{ diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs index c270c5bd10..1a563fef6d 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs @@ -218,6 +218,9 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do updated_provider = Repo.get!(Domain.Auth.Provider, provider.id) assert updated_provider.last_synced_at != provider.last_synced_at + + assert_receive {:bypass_request, %{request_path: "/admin/" <> _} = conn} + assert {"authorization", "Bearer OIDC_ACCESS_TOKEN"} in conn.req_headers end test "does not crash on endpoint errors" do diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs index 6ec1265cc3..054fa410da 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs @@ -83,24 +83,30 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do assert provider.name == attrs.name assert provider.adapter == attrs.adapter - assert provider.adapter_config == %{ - "scope" => - Enum.join( - [ - "openid", - "email", - "profile", - "https://www.googleapis.com/auth/admin.directory.orgunit.readonly", - "https://www.googleapis.com/auth/admin.directory.group.readonly", - "https://www.googleapis.com/auth/admin.directory.user.readonly" - ], - " " - ), + scope = + Enum.join( + [ + "openid", + "email", + "profile", + "https://www.googleapis.com/auth/admin.directory.orgunit.readonly", + "https://www.googleapis.com/auth/admin.directory.group.readonly", + "https://www.googleapis.com/auth/admin.directory.user.readonly" + ], + " " + ) + + assert %{ + "scope" => ^scope, "response_type" => "code", "client_id" => "client_id", - "client_secret" => "client_secret", - "discovery_document_uri" => discovery_document_url - } + "client_secret" => client_secret, + "discovery_document_uri" => ^discovery_document_url + } = provider.adapter_config + + assert client_secret + |> Base.decode64!() + |> Domain.Vault.decrypt!() == "client_secret" end end diff --git a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/refresh_access_tokens_test.exs b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/refresh_access_tokens_test.exs index 11e6186ed1..0ca0b63022 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/refresh_access_tokens_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/refresh_access_tokens_test.exs @@ -12,10 +12,10 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.RefreshAccessTokensTest do provider = Domain.Fixture.update!(provider, %{ adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => Fixtures.Auth.encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() |> DateTime.add(15, :minute), - "claims" => "openid email profile offline_access" + "claims" => %{} } }) @@ -54,7 +54,7 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.RefreshAccessTokensTest do "access_token" => "MY_ACCESS_TOKEN", "claims" => ^claims, "expires_at" => expires_at, - "refresh_token" => "OIDC_REFRESH_TOKEN", + "refresh_token" => refresh_token, "userinfo" => %{ "email" => "ada@example.com", "email_verified" => true, @@ -68,7 +68,27 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.RefreshAccessTokensTest do } } = provider.adapter_state + assert refresh_token + |> Base.decode64!() + |> Domain.Vault.decrypt!() == "OIDC_REFRESH_TOKEN" + assert expires_at + + assert_receive {:request, %{request_path: "/oauth/token"} = conn} + + adapter_config = + Ecto.embedded_load( + Domain.Auth.Adapters.MicrosoftEntra.ProviderConfig, + provider.adapter_config, + :json + ) + + assert conn.body_params == %{ + "client_id" => adapter_config.client_id, + "client_secret" => adapter_config.client_secret, + "grant_type" => "refresh_token", + "refresh_token" => "OIDC_REFRESH_TOKEN" + } end test "does not crash when endpoint it not available", %{ diff --git a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs index 43cbc02c2e..b90eec7cdd 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs @@ -124,6 +124,9 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.SyncDirectoryTest do updated_provider = Repo.get!(Domain.Auth.Provider, provider.id) assert updated_provider.last_synced_at != provider.last_synced_at + + assert_receive {:bypass_request, %{request_path: "/v1.0/groups"} = conn} + assert {"authorization", "Bearer OIDC_ACCESS_TOKEN"} in conn.req_headers end test "does not crash on endpoint errors" do diff --git a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra_test.exs b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra_test.exs index 220faf2a59..13f14aae9a 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra_test.exs @@ -84,26 +84,32 @@ defmodule Domain.Auth.Adapters.MicrosoftEntraTest do assert provider.name == attrs.name assert provider.adapter == attrs.adapter - assert provider.adapter_config == %{ - "scope" => - Enum.join( - [ - "openid", - "email", - "profile", - "offline_access", - "Group.Read.All", - "GroupMember.Read.All", - "User.Read", - "User.Read.All" - ], - " " - ), + scope = + Enum.join( + [ + "openid", + "email", + "profile", + "offline_access", + "Group.Read.All", + "GroupMember.Read.All", + "User.Read", + "User.Read.All" + ], + " " + ) + + assert %{ + "scope" => ^scope, "response_type" => "code", "client_id" => "client_id", - "client_secret" => "client_secret", - "discovery_document_uri" => discovery_document_url - } + "client_secret" => client_secret, + "discovery_document_uri" => ^discovery_document_url + } = provider.adapter_config + + assert client_secret + |> Base.decode64!() + |> Domain.Vault.decrypt!() == "client_secret" end end diff --git a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/refresh_access_tokens_test.exs b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/refresh_access_tokens_test.exs index 4bd60ece95..034da620b0 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/refresh_access_tokens_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/refresh_access_tokens_test.exs @@ -12,10 +12,10 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.RefreshAccessTokensTest do provider = Domain.Fixture.update!(provider, %{ adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => Fixtures.Auth.encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() |> DateTime.add(15, :minute), - "claims" => "openid email profile offline_access" + "claims" => %{} } }) @@ -54,7 +54,7 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.RefreshAccessTokensTest do "access_token" => "MY_ACCESS_TOKEN", "claims" => ^claims, "expires_at" => expires_at, - "refresh_token" => "OIDC_REFRESH_TOKEN", + "refresh_token" => refresh_token, "userinfo" => %{ "email" => "ada@example.com", "email_verified" => true, @@ -68,7 +68,27 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.RefreshAccessTokensTest do } } = provider.adapter_state + assert refresh_token + |> Base.decode64!() + |> Domain.Vault.decrypt!() == "OIDC_REFRESH_TOKEN" + assert expires_at + + assert_receive {:request, %{request_path: "/oauth/token"} = conn} + + adapter_config = + Ecto.embedded_load( + Domain.Auth.Adapters.Okta.ProviderConfig, + provider.adapter_config, + :json + ) + + assert conn.body_params == %{ + "client_id" => adapter_config.client_id, + "client_secret" => adapter_config.client_secret, + "grant_type" => "refresh_token", + "refresh_token" => "OIDC_REFRESH_TOKEN" + } end test "does not crash when endpoint is not available", %{ diff --git a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory.exs b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs similarity index 97% rename from elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory.exs rename to elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs index 2dc23ef8f2..61c6535ba2 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs @@ -229,7 +229,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do OktaDirectory.mock_group_members_list_endpoint(bypass, group["id"], members) end) - assert execute(%{}) == :ok + {:ok, pid} = Task.Supervisor.start_link() + assert execute(%{task_supervisor: pid}) == :ok groups = Actors.Group |> Repo.all() assert length(groups) == 2 @@ -268,12 +269,16 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do updated_provider = Repo.get!(Domain.Auth.Provider, provider.id) assert updated_provider.last_synced_at != provider.last_synced_at + + assert_receive {:bypass_request, %{request_path: "/api/v1/groups"} = conn} + assert {"authorization", "Bearer OIDC_ACCESS_TOKEN"} in conn.req_headers end test "does not crash on endpoint errors", %{bypass: bypass} do Bypass.down(bypass) - assert execute(%{}) == :ok + {:ok, pid} = Task.Supervisor.start_link() + assert execute(%{task_supervisor: pid}) == :ok assert Repo.aggregate(Actors.Group, :count) == 0 end @@ -323,7 +328,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do OktaDirectory.mock_groups_list_endpoint(bypass, []) OktaDirectory.mock_users_list_endpoint(bypass, users) - assert execute(%{}) == :ok + {:ok, pid} = Task.Supervisor.start_link() + assert execute(%{task_supervisor: pid}) == :ok assert updated_identity = Repo.get(Domain.Auth.Identity, identity.id) @@ -652,7 +658,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do one_member ) - assert execute(%{}) == :ok + {:ok, pid} = Task.Supervisor.start_link() + assert execute(%{task_supervisor: pid}) == :ok assert updated_group = Repo.get(Domain.Actors.Group, group.id) assert updated_group.name == "Group:Engineering" @@ -744,7 +751,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do end) end - assert execute(%{}) == :ok + {:ok, pid} = Task.Supervisor.start_link() + assert execute(%{task_supervisor: pid}) == :ok assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id) refute updated_provider.last_synced_at @@ -760,7 +768,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do end) end - assert execute(%{}) == :ok + {:ok, pid} = Task.Supervisor.start_link() + assert execute(%{task_supervisor: pid}) == :ok assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id) refute updated_provider.last_synced_at diff --git a/elixir/apps/domain/test/domain/auth/adapters/okta_test.exs b/elixir/apps/domain/test/domain/auth/adapters/okta_test.exs index 4b2787d881..63625db02c 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/okta_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/okta_test.exs @@ -90,26 +90,30 @@ defmodule Domain.Auth.Adapters.OktaTest do assert provider.name == attrs.name assert provider.adapter == attrs.adapter - assert provider.adapter_config == %{ - "scope" => - Enum.join( - [ - "openid", - "email", - "profile", - "offline_access", - "okta.groups.read", - "okta.users.read" - ], - " " - ), + scope = + Enum.join( + [ + "openid", + "email", + "profile", + "offline_access", + "okta.groups.read", + "okta.users.read" + ], + " " + ) + + assert %{ + "scope" => ^scope, "response_type" => "code", "client_id" => "client_id", - "client_secret" => "client_secret", - "discovery_document_uri" => discovery_document_url, - "okta_account_domain" => okta_account_domain, - "api_base_url" => api_base_url - } + "client_secret" => client_secret, + "discovery_document_uri" => ^discovery_document_url + } = provider.adapter_config + + assert client_secret + |> Base.decode64!() + |> Domain.Vault.decrypt!() == "client_secret" end end diff --git a/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs b/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs index 9480ce3c0f..ce54b63998 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs @@ -2,7 +2,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do use Domain.DataCase, async: true import Domain.Auth.Adapters.OpenIDConnect alias Domain.Auth - alias Domain.Auth.Adapters.OpenIDConnect.{PKCE, State} + alias Domain.Auth.Adapters.OpenIDConnect.{PKCE, ProviderConfig, ProviderState} describe "identity_changeset/2" do setup do @@ -11,7 +11,9 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do {provider, bypass} = Fixtures.Auth.start_and_create_openid_connect_provider(account: account) - changeset = %Auth.Identity{} |> Ecto.Changeset.change() + changeset = + %Auth.Identity{} + |> Ecto.Changeset.change() %{ bypass: bypass, @@ -104,13 +106,26 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do assert provider.name == attrs.name assert provider.adapter == attrs.adapter - assert provider.adapter_config == %{ + assert %{ "scope" => "openid email profile", "response_type" => "code", "client_id" => "client_id", - "client_secret" => "client_secret", - "discovery_document_uri" => discovery_document_url - } + "client_secret" => client_secret, + "discovery_document_uri" => ^discovery_document_url + } = provider.adapter_config + + assert client_secret + |> Base.decode64!() + |> Domain.Vault.decrypt!() == "client_secret" + + assert Ecto.embedded_load(ProviderConfig, provider.adapter_config, :json) == + %Domain.Auth.Adapters.OpenIDConnect.ProviderConfig{ + scope: "openid email profile", + response_type: "code", + client_id: "client_id", + client_secret: "client_secret", + discovery_document_uri: discovery_document_url + } end end @@ -173,13 +188,13 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do describe "ensure_states_equal/2" do test "returns ok when two states are equal" do - state = State.new() + state = authorization_uri_state() assert ensure_states_equal(state, state) == :ok end test "returns error when two states are equal" do - state1 = State.new() - state2 = State.new() + state1 = authorization_uri_state() + state2 = authorization_uri_state() assert ensure_states_equal(state1, state2) == {:error, :invalid_state} end end @@ -557,14 +572,18 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do provider = Repo.get(Domain.Auth.Provider, provider.id) |> Ecto.Changeset.change( - adapter_state: %{"refresh_token" => "foo", "access_token" => "bar"} + adapter_state: %{ + "refresh_token" => Fixtures.Auth.encode_secret!("foo"), + "access_token" => Fixtures.Auth.encode_secret!("bar") + } ) |> Repo.update!() assert refresh_access_token(provider) == {:error, :expired} provider = Repo.get(Domain.Auth.Provider, provider.id) - assert provider.adapter_state == %{"access_token" => "bar"} + provider = load(provider) + assert provider.adapter_state == %ProviderState{access_token: "bar"} end end end diff --git a/elixir/apps/domain/test/domain/auth_test.exs b/elixir/apps/domain/test/domain/auth_test.exs index 40a441e0b4..beeb51c876 100644 --- a/elixir/apps/domain/test/domain/auth_test.exs +++ b/elixir/apps/domain/test/domain/auth_test.exs @@ -321,8 +321,8 @@ defmodule Domain.AuthTest do Domain.Fixture.update!(provider, %{ disabled_at: DateTime.utc_now(), adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => Fixtures.Auth.encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() } }) @@ -336,8 +336,8 @@ defmodule Domain.AuthTest do Domain.Fixture.update!(provider, %{ deleted_at: DateTime.utc_now(), adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => Fixtures.Auth.encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() } }) @@ -351,9 +351,9 @@ defmodule Domain.AuthTest do Domain.Fixture.update!(provider, %{ provisioner: :manual, adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", - "claims" => "openid email profile offline_access", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => Fixtures.Auth.encode_secret!("OIDC_REFRESH_TOKEN"), + "claims" => %{}, "expires_at" => DateTime.utc_now() } }) @@ -366,10 +366,10 @@ defmodule Domain.AuthTest do Domain.Fixture.update!(provider, %{ adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => Fixtures.Auth.encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() |> DateTime.add(28, :minute), - "claims" => "openid email profile offline_access" + "claims" => %{} } }) @@ -384,10 +384,10 @@ defmodule Domain.AuthTest do Domain.Fixture.update!(provider, %{ adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", + "access_token" => Fixtures.Auth.encode_secret!("OIDC_ACCESS_TOKEN"), "refresh_token" => nil, "expires_at" => DateTime.utc_now() |> DateTime.add(28, :minute), - "claims" => "openid email profile offline_access" + "claims" => %{} } }) diff --git a/elixir/apps/domain/test/support/fixtures/auth.ex b/elixir/apps/domain/test/support/fixtures/auth.ex index 956cd58fe8..c715c2d913 100644 --- a/elixir/apps/domain/test/support/fixtures/auth.ex +++ b/elixir/apps/domain/test/support/fixtures/auth.ex @@ -90,7 +90,7 @@ defmodule Domain.Fixtures.Auth do openid_connect_adapter_config( discovery_document_uri: "http://localhost:#{bypass.port}/.well-known/openid-configuration", - scope: Domain.Auth.Adapters.GoogleWorkspace.Settings.scope() |> Enum.join(" ") + scope: Domain.Auth.Adapters.GoogleWorkspace.ProviderConfig.scope() |> Enum.join(" ") ) provider = @@ -108,7 +108,7 @@ defmodule Domain.Fixtures.Auth do openid_connect_adapter_config( discovery_document_uri: "http://localhost:#{bypass.port}/.well-known/openid-configuration", - scope: Domain.Auth.Adapters.MicrosoftEntra.Settings.scope() |> Enum.join(" ") + scope: Domain.Auth.Adapters.MicrosoftEntra.ProviderConfig.scope() |> Enum.join(" ") ) provider = @@ -128,7 +128,7 @@ defmodule Domain.Fixtures.Auth do api_base_url: api_base_url, okta_account_domain: api_base_url, discovery_document_uri: "#{api_base_url}/.well-known/openid-configuration", - scope: Domain.Auth.Adapters.Okta.Settings.scope() |> Enum.join(" ") + scope: Domain.Auth.Adapters.Okta.ProviderConfig.scope() |> Enum.join(" ") ) provider = @@ -185,10 +185,10 @@ defmodule Domain.Fixtures.Auth do update!(provider, disabled_at: nil, adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() |> DateTime.add(1, :day), - "claims" => "openid email profile offline_access" + "claims" => %{} } ) end @@ -212,10 +212,10 @@ defmodule Domain.Fixtures.Auth do update!(provider, disabled_at: nil, adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() |> DateTime.add(1, :day), - "claims" => "openid email profile offline_access" + "claims" => %{} } ) end @@ -239,10 +239,10 @@ defmodule Domain.Fixtures.Auth do update!(provider, disabled_at: nil, adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", + "access_token" => encode_secret!("OIDC_ACCESS_TOKEN"), + "refresh_token" => encode_secret!("OIDC_REFRESH_TOKEN"), "expires_at" => DateTime.utc_now() |> DateTime.add(1, :day), - "claims" => "openid email profile offline_access" + "claims" => %{} } ) end @@ -598,4 +598,9 @@ defmodule Domain.Fixtures.Auth do def add_permission(%Auth.Subject{} = subject, permission) do %{subject | permissions: MapSet.put(subject.permissions, permission)} end + + def encode_secret!(value) do + {:ok, value} = Domain.Types.EncryptedString.dump(value) + value + end end diff --git a/elixir/apps/domain/test/support/mocks/google_cloud_platform.ex b/elixir/apps/domain/test/support/mocks/google_cloud_platform.ex index 35cd797d91..98bceec6d5 100644 --- a/elixir/apps/domain/test/support/mocks/google_cloud_platform.ex +++ b/elixir/apps/domain/test/support/mocks/google_cloud_platform.ex @@ -6,7 +6,7 @@ defmodule Domain.Mocks.GoogleCloudPlatform do end def mock_instance_metadata_id_endpoint(bypass, id \\ Ecto.UUID.generate()) do - token_endpoint_path = "/instance/id" + token_endpoint_path = "instance/id" test_pid = self() @@ -16,13 +16,13 @@ defmodule Domain.Mocks.GoogleCloudPlatform do Plug.Conn.send_resp(conn, 200, id) end) - override_endpoint_url(:metadata_endpoint_url, "http://localhost:#{bypass.port}/") + override_endpoint_url(:metadata_endpoint_url, "http://localhost:#{bypass.port}") bypass end def mock_instance_metadata_zone_endpoint(bypass, zone \\ "projects/001001/zones/us-east-1") do - token_endpoint_path = "/instance/zone" + token_endpoint_path = "instance/zone" test_pid = self() @@ -32,13 +32,13 @@ defmodule Domain.Mocks.GoogleCloudPlatform do Plug.Conn.send_resp(conn, 200, zone) end) - override_endpoint_url(:metadata_endpoint_url, "http://localhost:#{bypass.port}/") + override_endpoint_url(:metadata_endpoint_url, "http://localhost:#{bypass.port}") bypass end def mock_instance_metadata_token_endpoint(bypass, resp \\ nil) do - token_endpoint_path = "/instance/service-accounts/default/token" + token_endpoint_path = "instance/service-accounts/default/token" resp = resp || @@ -56,7 +56,7 @@ defmodule Domain.Mocks.GoogleCloudPlatform do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url(:metadata_endpoint_url, "http://localhost:#{bypass.port}/") + override_endpoint_url(:metadata_endpoint_url, "http://localhost:#{bypass.port}") bypass end diff --git a/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex b/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex index 1ab2903fe1..3657d3e790 100644 --- a/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex +++ b/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex @@ -8,7 +8,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do end def mock_users_list_endpoint(bypass, users \\ nil) do - users_list_endpoint_path = "/admin/directory/v1/users" + users_list_endpoint_path = "admin/directory/v1/users" resp = %{ "kind" => "admin#directory#users", @@ -193,13 +193,13 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url("http://localhost:#{bypass.port}/") + override_endpoint_url("http://localhost:#{bypass.port}") bypass end def mock_organization_units_list_endpoint(bypass, org_units \\ nil) do - org_units_list_endpoint_path = "/admin/directory/v1/customer/my_customer/orgunits" + org_units_list_endpoint_path = "admin/directory/v1/customer/my_customer/orgunits" resp = %{ "kind" => "admin#directory#org_units", @@ -229,13 +229,13 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url("http://localhost:#{bypass.port}/") + override_endpoint_url("http://localhost:#{bypass.port}") bypass end def mock_groups_list_endpoint(bypass, groups \\ nil) do - groups_list_endpoint_path = "/admin/directory/v1/groups" + groups_list_endpoint_path = "admin/directory/v1/groups" resp = %{ "kind" => "admin#directory#groups", @@ -296,13 +296,13 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url("http://localhost:#{bypass.port}/") + override_endpoint_url("http://localhost:#{bypass.port}") bypass end def mock_group_members_list_endpoint(bypass, group_id, members \\ nil) do - group_members_list_endpoint_path = "/admin/directory/v1/groups/#{group_id}/members" + group_members_list_endpoint_path = "admin/directory/v1/groups/#{group_id}/members" resp = %{ "kind" => "admin#directory#members", @@ -366,7 +366,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url("http://localhost:#{bypass.port}/") + override_endpoint_url("http://localhost:#{bypass.port}") bypass end diff --git a/elixir/apps/domain/test/support/mocks/microsoft_entra_directory.ex b/elixir/apps/domain/test/support/mocks/microsoft_entra_directory.ex index 5798ca5eb0..b8270183df 100644 --- a/elixir/apps/domain/test/support/mocks/microsoft_entra_directory.ex +++ b/elixir/apps/domain/test/support/mocks/microsoft_entra_directory.ex @@ -54,7 +54,7 @@ defmodule Domain.Mocks.MicrosoftEntraDirectory do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url("http://localhost:#{bypass.port}/") + override_endpoint_url("http://localhost:#{bypass.port}") bypass end @@ -90,7 +90,7 @@ defmodule Domain.Mocks.MicrosoftEntraDirectory do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url("http://localhost:#{bypass.port}/") + override_endpoint_url("http://localhost:#{bypass.port}") bypass end @@ -136,7 +136,7 @@ defmodule Domain.Mocks.MicrosoftEntraDirectory do Plug.Conn.send_resp(conn, 200, Jason.encode!(resp)) end) - override_endpoint_url("http://localhost:#{bypass.port}/") + override_endpoint_url("http://localhost:#{bypass.port}") bypass end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/okta/components.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/okta/components.ex index 92bdde271a..fd7f9cefc8 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/okta/components.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/okta/components.ex @@ -119,7 +119,7 @@ defmodule Web.Settings.IdentityProviders.Okta.Components do end def scopes do - Domain.Auth.Adapters.Okta.Settings.scope() + Domain.Auth.Adapters.Okta.ProviderConfig.scope() |> Enum.join("\n") end diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/edit_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/edit_test.exs index 3eee8bb704..ac05655b44 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/edit_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/edit_test.exs @@ -100,6 +100,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.EditTest do render_submit(form) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.GoogleWorkspace.load(provider) assert_redirected( lv, @@ -109,8 +110,8 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.EditTest do assert provider.name == provider_attrs.name assert provider.adapter == :google_workspace - assert provider.adapter_config["client_id"] == adapter_config_attrs["client_id"] - assert provider.adapter_config["client_secret"] == adapter_config_attrs["client_secret"] + assert provider.adapter_config.client_id == adapter_config_attrs["client_id"] + assert provider.adapter_config.client_secret == adapter_config_attrs["client_secret"] end test "renders changeset errors on invalid attrs", %{ diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/new_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/new_test.exs index d2b2d2d02c..14012ee7cf 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/new_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/new_test.exs @@ -90,6 +90,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.NewTest do render_submit(form) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.GoogleWorkspace.load(provider) assert_redirected( lv, @@ -99,11 +100,8 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.NewTest do assert provider.name == provider_attrs.name assert provider.adapter == :google_workspace - assert provider.adapter_config["client_id"] == - provider_attrs.adapter_config["client_id"] - - assert provider.adapter_config["client_secret"] == - provider_attrs.adapter_config["client_secret"] + assert provider.adapter_config.client_id == provider_attrs.adapter_config["client_id"] + assert provider.adapter_config.client_secret == provider_attrs.adapter_config["client_secret"] end test "renders changeset errors on invalid attrs", %{ diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/edit_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/edit_test.exs index 6575bc994b..bd37be96c1 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/edit_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/edit_test.exs @@ -99,6 +99,7 @@ defmodule Web.Live.Settings.IdentityProviders.MicrosoftEntra.EditTest do render_submit(form) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.MicrosoftEntra.load(provider) assert_redirected( lv, @@ -108,8 +109,8 @@ defmodule Web.Live.Settings.IdentityProviders.MicrosoftEntra.EditTest do assert provider.name == provider_attrs.name assert provider.adapter == :microsoft_entra - assert provider.adapter_config["client_id"] == adapter_config_attrs["client_id"] - assert provider.adapter_config["client_secret"] == adapter_config_attrs["client_secret"] + assert provider.adapter_config.client_id == adapter_config_attrs["client_id"] + assert provider.adapter_config.client_secret == adapter_config_attrs["client_secret"] end test "renders changeset errors on invalid attrs", %{ diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/new_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/new_test.exs index d40b4198a5..1d6984fb2d 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/new_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/microsoft_entra/new_test.exs @@ -90,6 +90,7 @@ defmodule Web.Live.Settings.IdentityProviders.MicrosoftEntra.NewTest do render_submit(form) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.MicrosoftEntra.load(provider) assert_redirected( lv, @@ -99,11 +100,8 @@ defmodule Web.Live.Settings.IdentityProviders.MicrosoftEntra.NewTest do assert provider.name == provider_attrs.name assert provider.adapter == :microsoft_entra - assert provider.adapter_config["client_id"] == - provider_attrs.adapter_config["client_id"] - - assert provider.adapter_config["client_secret"] == - provider_attrs.adapter_config["client_secret"] + assert provider.adapter_config.client_id == provider_attrs.adapter_config["client_id"] + assert provider.adapter_config.client_secret == provider_attrs.adapter_config["client_secret"] end test "renders changeset errors on invalid attrs", %{ diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/okta/edit_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/okta/edit_test.exs index 461c4304f0..4f8fcc8da8 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/okta/edit_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/okta/edit_test.exs @@ -105,6 +105,7 @@ defmodule Web.Live.Settings.IdentityProviders.Okta.EditTest do }) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.Okta.load(provider) assert_redirected( lv, @@ -114,12 +115,11 @@ defmodule Web.Live.Settings.IdentityProviders.Okta.EditTest do assert provider.name == provider_attrs.name assert provider.adapter == :okta - assert provider.adapter_config["client_id"] == adapter_config_attrs["client_id"] - assert provider.adapter_config["client_secret"] == adapter_config_attrs["client_secret"] + assert provider.adapter_config.client_id == adapter_config_attrs["client_id"] + assert provider.adapter_config.client_secret == adapter_config_attrs["client_secret"] + assert provider.adapter_config.okta_account_domain == api_base_url - assert provider.adapter_config["okta_account_domain"] == api_base_url - - assert provider.adapter_config["discovery_document_uri"] == + assert provider.adapter_config.discovery_document_uri == "#{api_base_url}/.well-known/openid-configuration" end diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/okta/new_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/okta/new_test.exs index ca26d48096..5373741126 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/okta/new_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/okta/new_test.exs @@ -95,6 +95,7 @@ defmodule Web.Live.Settings.IdentityProviders.Okta.NewTest do }) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.Okta.load(provider) assert_redirected( lv, @@ -104,11 +105,8 @@ defmodule Web.Live.Settings.IdentityProviders.Okta.NewTest do assert provider.name == provider_attrs.name assert provider.adapter == :okta - assert provider.adapter_config["client_id"] == - provider_attrs.adapter_config["client_id"] - - assert provider.adapter_config["client_secret"] == - provider_attrs.adapter_config["client_secret"] + assert provider.adapter_config.client_id == provider_attrs.adapter_config["client_id"] + assert provider.adapter_config.client_secret == provider_attrs.adapter_config["client_secret"] end test "renders changeset errors on invalid attrs", %{ diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs index 61aa02151e..8552d7f503 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/edit_test.exs @@ -97,6 +97,7 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.EditTest do render_submit(form) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.OpenIDConnect.load(provider) assert_redirected( lv, @@ -106,12 +107,12 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.EditTest do assert provider.name == provider_attrs.name assert provider.adapter == :openid_connect - assert provider.adapter_config == %{ - "client_id" => provider_attrs.adapter_config["client_id"], - "client_secret" => provider_attrs.adapter_config["client_secret"], - "discovery_document_uri" => provider_attrs.adapter_config["discovery_document_uri"], - "scope" => provider_attrs.adapter_config["scope"], - "response_type" => "code" + assert provider.adapter_config == %Domain.Auth.Adapters.OpenIDConnect.ProviderConfig{ + client_id: provider_attrs.adapter_config["client_id"], + client_secret: provider_attrs.adapter_config["client_secret"], + discovery_document_uri: provider_attrs.adapter_config["discovery_document_uri"], + scope: provider_attrs.adapter_config["scope"], + response_type: "code" } end diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/new_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/new_test.exs index e7fb4a7fb2..efe8476b27 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/new_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/new_test.exs @@ -92,6 +92,7 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.NewTest do render_submit(form) assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name) + provider = Domain.Auth.Adapters.OpenIDConnect.load(provider) assert_redirected( lv, @@ -101,12 +102,12 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.NewTest do assert provider.name == provider_attrs.name assert provider.adapter == :openid_connect - assert provider.adapter_config == %{ - "client_id" => provider_attrs.adapter_config["client_id"], - "client_secret" => provider_attrs.adapter_config["client_secret"], - "discovery_document_uri" => provider_attrs.adapter_config["discovery_document_uri"], - "scope" => provider_attrs.adapter_config["scope"], - "response_type" => "code" + assert provider.adapter_config == %Domain.Auth.Adapters.OpenIDConnect.ProviderConfig{ + client_id: provider_attrs.adapter_config["client_id"], + client_secret: provider_attrs.adapter_config["client_secret"], + discovery_document_uri: provider_attrs.adapter_config["discovery_document_uri"], + scope: provider_attrs.adapter_config["scope"], + response_type: "code" } end diff --git a/elixir/config/config.exs b/elixir/config/config.exs index adabdaf643..5fed4bb24a 100644 --- a/elixir/config/config.exs +++ b/elixir/config/config.exs @@ -29,6 +29,20 @@ config :domain, Domain.Repo, migration_timestamps: [type: :timestamptz], start_apps_before_migration: [:ssl, :logger_json] +config :domain, + cloak_repo: Domain.Repo, + cloak_schemas: [ + Domain.Auth.Provider, + Domain.Auth.Identity + ] + +config :domain, Domain.Vault, + ciphers: [ + default: + {Cloak.Ciphers.AES.GCM, + tag: "AES.GCM.V1", key: Base.decode64!("h/spvF1P7omufNosGWWTnaC5gjupsAYiK54ZcjWjP3w=")} + ] + config :domain, Domain.Tokens, key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2", salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index 7402331b80..a664e1ab2d 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -18,6 +18,14 @@ if config_env() == :prod do ssl_opts: compile_config!(:database_ssl_opts), parameters: compile_config!(:database_parameters) + ciphers = + for key <- compile_config!(:database_encryption_keys) do + label = Domain.Crypto.hash(:sha1, key) |> String.slice(0, 8) + {label, {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: key}} + end + + config :domain, Domain.Vault, ciphers: ciphers + config :domain, Domain.Tokens, key_base: compile_config!(:tokens_key_base), salt: compile_config!(:tokens_salt) diff --git a/elixir/mix.lock b/elixir/mix.lock index dc05b03e2d..c1f0a78b42 100644 --- a/elixir/mix.lock +++ b/elixir/mix.lock @@ -1,14 +1,16 @@ %{ "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, "argon2_elixir": {:hex, :argon2_elixir, "4.0.0", "7f6cd2e4a93a37f61d58a367d82f830ad9527082ff3c820b8197a8a736648941", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f9da27cf060c9ea61b1bd47837a28d7e48a8f6fa13a745e252556c14f9132c7f"}, - "bandit": {:hex, :bandit, "1.4.2", "a1475c8dcbffd1f43002797f99487a64c8444753ff2b282b52409e279488e1f5", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3db8bacea631bd926cc62ccad58edfee4252d1b4c5cccbbad9825df2722b884f"}, + "bandit": {:hex, :bandit, "1.5.0", "3bc864a0da7f013ad3713a7f550c6a6ec0e19b8d8715ec678256a0dc197d5539", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "92d18d9a7228a597e0d4661ef69a874ea82d63ff49c7d801a5c68cb18ebbbd72"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bureaucrat": {:hex, :bureaucrat, "0.2.10", "b0de157dad540e40007b663b683f716ced21f85ff0591093aadb209ad0d967e1", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "bc7e5162b911c29c8ebefee87a2c16fbf13821a58f448a8fd024eb6c17fae15c"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, - "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"}, + "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, - "cldr_utils": {:hex, :cldr_utils, "2.24.2", "364fa30be55d328e704629568d431eb74cd2f085752b27f8025520b566352859", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3362b838836a9f0fa309de09a7127e36e67310e797d556db92f71b548832c7cf"}, + "cldr_utils": {:hex, :cldr_utils, "2.25.0", "3cc2ab6e9e4f855ba78a3f3fc4963ccf7b68b731f4e91de3d9b310adddb96b62", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "9041660356ffa1129e0d87d110e188f5da0e0bba94fb915e11275e04ace066e1"}, + "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, + "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, @@ -25,11 +27,11 @@ "elixir_make": {:hex, :elixir_make, "0.8.3", "d38d7ee1578d722d89b4d452a3e36bcfdc644c618f0d063b874661876e708683", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "5c99a18571a756d4af7a4d89ca75c28ac899e6103af6f223982f09ce44942cc9"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, - "ex_cldr": {:hex, :ex_cldr, "2.37.5", "9da6d97334035b961d2c2de167dc6af8cd3e09859301a5b8f49f90bd8b034593", [:mix], [{:cldr_utils, "~> 2.21", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "74ad5ddff791112ce4156382e171a5f5d3766af9d5c4675e0571f081fe136479"}, + "ex_cldr": {:hex, :ex_cldr, "2.38.0", "80399ccbfd996ea02f245394db83d7ce6113ed88e1e50e4695238cd9a0c258d5", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "8758000c97bdf4b2583c3fedd7cfa35896567a7f8351248b2faa33ba73841cc7"}, "ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.23.1", "822739e588ab2fe64de37f40bbda4f05e45a3d3d444373a56d5376dcc4e2e454", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: true]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.16", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2983f002016c283d14ee838d1950d2f9a1e58e8b19a2145d95079874fe6de10d"}, - "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.15.1", "e92ba17c41e7405b7784e0e65f406b5f17cfe313e0e70de9befd653e12854822", [:mix], [{:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "31df8bd37688340f8819bdd770eb17d659652078d34db632b85d4a32864d6a25"}, - "ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.16.0", "d9848a5de83b6f1bcba151cc43d63b5c6311813cd605b1df1afd896dfdd21001", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.22", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "0f2f250d479cadda4e0ef3a5e3d936ae7ba1a3f1199db6791e284e86203495b1"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.32.4", "5562148dfc631b04712983975093d2aac29df30b3bf2f7257e0c94b85b72e91b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6fd5a82f0785418fa8b698c0be2b1845dff92b77f1b3172c763d37868fb503d2"}, + "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.1", "29317f533cb5ec046d04523256cca4090291e9157028f28731395149b06ff8b2", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "095d5e973bf0ee066dd1153990d10cb6fa6d8ff0e028295bdce7a7821c70a0e4"}, + "ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.17.0", "6e7ef87aa5cfa062aabf6dacb25d91fc66d7304f69e974dcf0bc612deb6b5314", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.23", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.33", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "8377501cc6245ad235ee765bcab455e9e8c3f53cc5d775c094bf2cebb641e7ed"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.33.1", "49dc6e77e6d9ad22660aaa2480a7408ad3aedfbe517e4e83e5fe3a7bf5345770", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "c003bfaa3fdee6bab5195f128b94038c2ce1cf4879a759eef431dd075d9a5dac"}, "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "file_size": {:hex, :file_size, "3.0.1", "ad447a69442a82fc701765a73992d7b1110136fa0d4a9d3190ea685d60034dcd", [:mix], [{:decimal, ">= 1.0.0 and < 3.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:number, "~> 1.0", [hex: :number, repo: "hexpm", optional: false]}], "hexpm", "64dd665bc37920480c249785788265f5d42e98830d757c6a477b3246703b8e20"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, @@ -46,20 +48,20 @@ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "jose": {:hex, :jose, "1.11.9", "c861eb99d9e9f62acd071dc5a49ffbeab9014e44490cd85ea3e49e3d36184777", [:mix, :rebar3], [], "hexpm", "b5ccc3749d2e1638c26bed806259df5bc9e438797fe60dc71e9fa0716133899b"}, + "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"}, "libcluster": {:hex, :libcluster, "3.3.3", "a4f17721a19004cfc4467268e17cff8b1f951befe428975dd4f6f7b84d927fe0", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7c0a2275a0bb83c07acd17dab3c3bfb4897b145106750eeccc62d302e3bdfee5"}, - "logger_json": {:git, "https://github.com/nebo15/logger_json.git", "1d998ba0172887906b35e7110c74f0d80ac29a56", []}, + "logger_json": {:git, "https://github.com/nebo15/logger_json.git", "29bc59915e108ea21511d4e490db6e088017334c", []}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, + "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, "multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"}, "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "number": {:hex, :number, "1.0.4", "3e6e6032a3c1d4c3760e77a42c580a57a15545dd993af380809da30fe51a032c", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "16f7516584ef2be812af4f33f2eaf3f9b9f6ed8892f45853eb93113f83721e42"}, + "number": {:hex, :number, "1.0.5", "d92136f9b9382aeb50145782f116112078b3465b7be58df1f85952b8bb399b0f", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c0733a0a90773a66582b9e92a3f01290987f395c972cb7d685f51dd927cd5169"}, "observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"}, "openid_connect": {:git, "https://github.com/firezone/openid_connect.git", "76c1a1c9a9da3b8be7b8270306a9240d80d7696f", [ref: "76c1a1c9a9da3b8be7b8270306a9240d80d7696f"]}, "opentelemetry": {:hex, :opentelemetry, "1.4.0", "f928923ed80adb5eb7894bac22e9a198478e6a8f04020ae1d6f289fdcad0b498", [:rebar3], [{:opentelemetry_api, "~> 1.3.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "50b32ce127413e5d87b092b4d210a3449ea80cd8224090fe68d73d576a3faa15"}, @@ -78,7 +80,7 @@ "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.2", "354460993a480656b71c3887f5565f612b3bdbdd8688c83f9e6f512307067dd4", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "2bb3722f327e14a7aa47b1acf27ed633c8cd27b167e18b8237954b9b4804af39"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.14", "70fa101aa0539e81bed4238777498f6215e9dda3461bdaa067cad6908110c364", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "82f6d006c5264f979ed5eb75593d808bbe39020f20df2e78426f4f2d570e2402"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, @@ -94,13 +96,13 @@ "sizeable": {:hex, :sizeable, "1.0.2", "625fe06a5dad188b52121a140286f1a6ae1adf350a942cf419499ecd8a11ee29", [:mix], [], "hexpm", "4bab548e6dfba777b400ca50830a9e3a4128e73df77ab1582540cf5860601762"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "swoosh": {:hex, :swoosh, "1.16.3", "4ab7dc429e84afaf8ffe1c7c06ce1acbc7ddde758d2cb9152dd2ac32289d5498", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.1.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ff70980087650a72951ebd109a286d83c270e2b6610aba447140562adff8cf0a"}, + "swoosh": {:hex, :swoosh, "1.16.4", "d407768b3b68e3d1ff8d43b575a20c13bea338647143e241a324894cdb5af0b2", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.1.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a38798368c09b09d7108803c42f24bb051d3e87bc1b81e6f09b20bf5a31c6676"}, "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"}, - "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [: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.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [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", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, + "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [: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.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [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", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"}, "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "tls_certificate_check": {:hex, :tls_certificate_check, "1.22.1", "0f450cc1568a67a65ce5e15df53c53f9a098c3da081c5f126199a72505858dc1", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3092be0babdc0e14c2e900542351e066c0fa5a9cf4b3597559ad1e67f07938c0"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, diff --git a/elixir/rel/overlays/bin/migrate_encryption_keys b/elixir/rel/overlays/bin/migrate_encryption_keys new file mode 100755 index 0000000000..2768e36ada --- /dev/null +++ b/elixir/rel/overlays/bin/migrate_encryption_keys @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +. "$(dirname -- "$0")/bootstrap" +exec ./"$APPLICATION_NAME" eval Domain.Vault.migrate_keys