From 8edbbac95f3f1ff43b5467a10a677f04acf4caf1 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 24 Feb 2026 13:55:19 +0100 Subject: [PATCH] feat: OIDC configuration in global Settings (ENV or DB) - Add oidc_* attributes to Setting, migration and Config helpers - Secrets and OidcRoleSyncConfig read from Config (ENV overrides DB) - GlobalSettingsLive: OIDC section with disabled fields when ENV set - OIDC role sync tests use DataCase for DB access --- lib/membership/setting.ex | 54 ++++- lib/mv/config.ex | 77 ++++++ lib/mv/oidc_role_sync_config.ex | 16 +- lib/mv/secrets.ex | 27 +-- lib/mv_web/live/global_settings_live.ex | 224 ++++++++++-------- .../20260224122831_add_oidc_to_settings.exs | 29 +++ .../repo/settings/20260224122831.json | 164 +++++++++++++ test/mv/oidc_role_sync_config_test.exs | 32 ++- 8 files changed, 487 insertions(+), 136 deletions(-) create mode 100644 priv/repo/migrations/20260224122831_add_oidc_to_settings.exs create mode 100644 priv/resource_snapshots/repo/settings/20260224122831.json diff --git a/lib/membership/setting.ex b/lib/membership/setting.ex index 154288b..6e987de 100644 --- a/lib/membership/setting.ex +++ b/lib/membership/setting.ex @@ -79,7 +79,13 @@ defmodule Mv.Membership.Setting do :vereinfacht_api_url, :vereinfacht_api_key, :vereinfacht_club_id, - :vereinfacht_app_url + :vereinfacht_app_url, + :oidc_client_id, + :oidc_base_url, + :oidc_redirect_uri, + :oidc_client_secret, + :oidc_admin_group_name, + :oidc_groups_claim ] end @@ -96,7 +102,13 @@ defmodule Mv.Membership.Setting do :vereinfacht_api_url, :vereinfacht_api_key, :vereinfacht_club_id, - :vereinfacht_app_url + :vereinfacht_app_url, + :oidc_client_id, + :oidc_base_url, + :oidc_redirect_uri, + :oidc_client_secret, + :oidc_admin_group_name, + :oidc_groups_claim ] end @@ -322,6 +334,44 @@ defmodule Mv.Membership.Setting do description "Vereinfacht app base URL for contact view links (e.g. https://app.verein.visuel.dev)" end + # OIDC authentication (can be overridden by ENV) + attribute :oidc_client_id, :string do + allow_nil? true + public? true + description "OIDC client ID (e.g. from OIDC_CLIENT_ID)" + end + + attribute :oidc_base_url, :string do + allow_nil? true + public? true + description "OIDC provider base URL (e.g. from OIDC_BASE_URL)" + end + + attribute :oidc_redirect_uri, :string do + allow_nil? true + public? true + description "OIDC redirect URI for callback (e.g. from OIDC_REDIRECT_URI)" + end + + attribute :oidc_client_secret, :string do + allow_nil? true + public? false + description "OIDC client secret (e.g. from OIDC_CLIENT_SECRET)" + sensitive? true + end + + attribute :oidc_admin_group_name, :string do + allow_nil? true + public? true + description "OIDC group name that maps to Admin role (e.g. from OIDC_ADMIN_GROUP_NAME)" + end + + attribute :oidc_groups_claim, :string do + allow_nil? true + public? true + description "JWT claim name for group list (e.g. from OIDC_GROUPS_CLAIM, default 'groups')" + end + timestamps() end diff --git a/lib/mv/config.ex b/lib/mv/config.ex index d2ad66c..f70a07e 100644 --- a/lib/mv/config.ex +++ b/lib/mv/config.ex @@ -263,6 +263,10 @@ defmodule Mv.Config do end defp get_vereinfacht_from_settings(key) do + get_from_settings(key) + end + + defp get_from_settings(key) do case Mv.Membership.get_settings() do {:ok, settings} -> settings |> Map.get(key) |> trim_nil() {:error, _} -> nil @@ -298,4 +302,77 @@ defmodule Mv.Config do defp present?(nil), do: false defp present?(s) when is_binary(s), do: String.trim(s) != "" defp present?(_), do: false + + # --------------------------------------------------------------------------- + # OIDC authentication + # ENV variables take priority; fallback to Settings from database. + # --------------------------------------------------------------------------- + + @doc """ + Returns the OIDC client ID. ENV first, then Settings. + """ + @spec oidc_client_id() :: String.t() | nil + def oidc_client_id do + env_or_setting("OIDC_CLIENT_ID", :oidc_client_id) + end + + @doc """ + Returns the OIDC provider base URL. ENV first, then Settings. + """ + @spec oidc_base_url() :: String.t() | nil + def oidc_base_url do + env_or_setting("OIDC_BASE_URL", :oidc_base_url) + end + + @doc """ + Returns the OIDC redirect URI. ENV first, then Settings. + """ + @spec oidc_redirect_uri() :: String.t() | nil + def oidc_redirect_uri do + env_or_setting("OIDC_REDIRECT_URI", :oidc_redirect_uri) + end + + @doc """ + Returns the OIDC client secret. ENV first, then Settings. + """ + @spec oidc_client_secret() :: String.t() | nil + def oidc_client_secret do + env_or_setting("OIDC_CLIENT_SECRET", :oidc_client_secret) + end + + @doc """ + Returns the OIDC admin group name (for role sync). ENV first, then Settings. + """ + @spec oidc_admin_group_name() :: String.t() | nil + def oidc_admin_group_name do + env_or_setting("OIDC_ADMIN_GROUP_NAME", :oidc_admin_group_name) + end + + @doc """ + Returns the OIDC groups claim name (default "groups"). ENV first, then Settings. + """ + @spec oidc_groups_claim() :: String.t() | nil + def oidc_groups_claim do + case env_or_setting("OIDC_GROUPS_CLAIM", :oidc_groups_claim) do + nil -> "groups" + v -> v + end + end + + @doc """ + Returns true if any OIDC ENV variable is set (used to show hint in Settings UI). + """ + @spec oidc_env_configured?() :: boolean() + def oidc_env_configured? do + oidc_client_id_env_set?() or oidc_base_url_env_set?() or + oidc_redirect_uri_env_set?() or oidc_client_secret_env_set?() or + oidc_admin_group_name_env_set?() or oidc_groups_claim_env_set?() + end + + def oidc_client_id_env_set?, do: env_set?("OIDC_CLIENT_ID") + def oidc_base_url_env_set?, do: env_set?("OIDC_BASE_URL") + def oidc_redirect_uri_env_set?, do: env_set?("OIDC_REDIRECT_URI") + def oidc_client_secret_env_set?, do: env_set?("OIDC_CLIENT_SECRET") + def oidc_admin_group_name_env_set?, do: env_set?("OIDC_ADMIN_GROUP_NAME") + def oidc_groups_claim_env_set?, do: env_set?("OIDC_GROUPS_CLAIM") end diff --git a/lib/mv/oidc_role_sync_config.ex b/lib/mv/oidc_role_sync_config.ex index 493a435..2a8574c 100644 --- a/lib/mv/oidc_role_sync_config.ex +++ b/lib/mv/oidc_role_sync_config.ex @@ -2,23 +2,19 @@ defmodule Mv.OidcRoleSyncConfig do @moduledoc """ Runtime configuration for OIDC group → role sync (e.g. admin group → Admin role). - Reads from Application config `:mv, :oidc_role_sync`: - - `:admin_group_name` – OIDC group name that maps to Admin role (optional; when nil, no sync). - - `:groups_claim` – JWT/user_info claim name for groups (default: `"groups"`). + Reads from Mv.Config (ENV first, then Settings): + - `oidc_admin_group_name/0` – OIDC group name that maps to Admin role (optional; when nil, no sync). + - `oidc_groups_claim/0` – JWT/user_info claim name for groups (default: `"groups"`). - Set via ENV in production: OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM (see config/runtime.exs). + Set via ENV: OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM; or via Settings (Basic settings → OIDC). """ @doc "Returns the OIDC group name that maps to Admin role, or nil if not configured." def oidc_admin_group_name do - get(:admin_group_name) + Mv.Config.oidc_admin_group_name() end @doc "Returns the JWT/user_info claim name for groups; defaults to \"groups\"." def oidc_groups_claim do - get(:groups_claim) || "groups" - end - - defp get(key) do - Application.get_env(:mv, :oidc_role_sync, []) |> Keyword.get(key) + Mv.Config.oidc_groups_claim() || "groups" end end diff --git a/lib/mv/secrets.ex b/lib/mv/secrets.ex index d3bc30d..f315ea3 100644 --- a/lib/mv/secrets.ex +++ b/lib/mv/secrets.ex @@ -7,12 +7,12 @@ defmodule Mv.Secrets do particularly for OIDC (Rauthy) authentication. ## Configuration Source - Secrets are read from the `:oidc` key in the application configuration, - which is typically set in `config/runtime.exs` from environment variables: - - `OIDC_CLIENT_ID` - - `OIDC_CLIENT_SECRET` - - `OIDC_BASE_URL` - - `OIDC_REDIRECT_URI` + Secrets are read via `Mv.Config` which prefers environment variables and + falls back to Settings from the database: + - OIDC_CLIENT_ID / settings.oidc_client_id + - OIDC_CLIENT_SECRET / settings.oidc_client_secret + - OIDC_BASE_URL / settings.oidc_base_url + - OIDC_REDIRECT_URI / settings.oidc_redirect_uri ## Usage This module is automatically called by AshAuthentication when resolving @@ -26,7 +26,7 @@ defmodule Mv.Secrets do _opts, _meth ) do - get_config(:client_id) + {:ok, Mv.Config.oidc_client_id()} end def secret_for( @@ -35,7 +35,7 @@ defmodule Mv.Secrets do _opts, _meth ) do - get_config(:redirect_uri) + {:ok, Mv.Config.oidc_redirect_uri()} end def secret_for( @@ -44,7 +44,7 @@ defmodule Mv.Secrets do _opts, _meth ) do - get_config(:client_secret) + {:ok, Mv.Config.oidc_client_secret()} end def secret_for( @@ -53,13 +53,6 @@ defmodule Mv.Secrets do _opts, _meth ) do - get_config(:base_url) - end - - defp get_config(key) do - :mv - |> Application.fetch_env!(:oidc) - |> Keyword.fetch!(key) - |> then(&{:ok, &1}) + {:ok, Mv.Config.oidc_base_url()} end end diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index c710f79..b67b6ac 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -42,7 +42,6 @@ defmodule MvWeb.GlobalSettingsLive do socket |> assign(:page_title, gettext("Settings")) |> assign(:settings, settings) - |> assign(:active_editing_section, nil) |> assign(:locale, locale) |> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?()) |> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?()) @@ -52,6 +51,14 @@ defmodule MvWeb.GlobalSettingsLive do |> assign(:vereinfacht_api_key_set, present?(settings.vereinfacht_api_key)) |> assign(:last_vereinfacht_sync_result, nil) |> assign(:vereinfacht_test_result, nil) + |> assign(:oidc_env_configured, Mv.Config.oidc_env_configured?()) + |> assign(:oidc_client_id_env_set, Mv.Config.oidc_client_id_env_set?()) + |> assign(:oidc_base_url_env_set, Mv.Config.oidc_base_url_env_set?()) + |> assign(:oidc_redirect_uri_env_set, Mv.Config.oidc_redirect_uri_env_set?()) + |> assign(:oidc_client_secret_env_set, Mv.Config.oidc_client_secret_env_set?()) + |> assign(:oidc_admin_group_name_env_set, Mv.Config.oidc_admin_group_name_env_set?()) + |> assign(:oidc_groups_claim_env_set, Mv.Config.oidc_groups_claim_env_set?()) + |> assign(:oidc_client_secret_set, present?(settings.oidc_client_secret)) |> assign_form() {:ok, socket} @@ -196,21 +203,110 @@ defmodule MvWeb.GlobalSettingsLive do <% end %> - <%!-- Memberdata Section --%> - <.form_section title={gettext("Memberdata")}> - <.live_component - :if={@active_editing_section != :custom_fields} - module={MvWeb.MemberFieldLive.IndexComponent} - id="member-fields-component" - settings={@settings} - /> - <%!-- Custom Fields Section --%> - <.live_component - :if={@active_editing_section != :member_fields} - module={MvWeb.CustomFieldLive.IndexComponent} - id="custom-fields-component" - actor={@current_user} - /> + <%!-- OIDC Section --%> + <.form_section title={gettext("OIDC")}> + <%= if @oidc_env_configured do %> +

+ {gettext("Some values are set via environment variables. Those fields are read-only.")} +

+ <% end %> + <.form for={@form} id="oidc-form" phx-change="validate" phx-submit="save"> +
+ <.input + field={@form[:oidc_client_id]} + type="text" + label={gettext("Client ID")} + disabled={@oidc_client_id_env_set} + placeholder={ + if(@oidc_client_id_env_set, do: gettext("From OIDC_CLIENT_ID"), else: "mv") + } + /> + <.input + field={@form[:oidc_base_url]} + type="text" + label={gettext("Base URL")} + disabled={@oidc_base_url_env_set} + placeholder={ + if(@oidc_base_url_env_set, + do: gettext("From OIDC_BASE_URL"), + else: "http://localhost:8080/auth/v1" + ) + } + /> + <.input + field={@form[:oidc_redirect_uri]} + type="text" + label={gettext("Redirect URI")} + disabled={@oidc_redirect_uri_env_set} + placeholder={ + if(@oidc_redirect_uri_env_set, + do: gettext("From OIDC_REDIRECT_URI"), + else: "http://localhost:4000/auth/user/oidc/callback" + ) + } + /> +
+ + <.input + field={@form[:oidc_client_secret]} + type="password" + label="" + disabled={@oidc_client_secret_env_set} + placeholder={ + if(@oidc_client_secret_env_set, + do: gettext("From OIDC_CLIENT_SECRET"), + else: + if(@oidc_client_secret_set, + do: gettext("Leave blank to keep current"), + else: nil + ) + ) + } + /> +
+ <.input + field={@form[:oidc_admin_group_name]} + type="text" + label={gettext("Admin group name")} + disabled={@oidc_admin_group_name_env_set} + placeholder={ + if(@oidc_admin_group_name_env_set, + do: gettext("From OIDC_ADMIN_GROUP_NAME"), + else: gettext("e.g. admin") + ) + } + /> + <.input + field={@form[:oidc_groups_claim]} + type="text" + label={gettext("Groups claim")} + disabled={@oidc_groups_claim_env_set} + placeholder={ + if(@oidc_groups_claim_env_set, + do: gettext("From OIDC_GROUPS_CLAIM"), + else: "groups" + ) + } + /> +
+ <.button + :if={ + not (@oidc_client_id_env_set and @oidc_base_url_env_set and + @oidc_redirect_uri_env_set and @oidc_client_secret_env_set and + @oidc_admin_group_name_env_set and @oidc_groups_claim_env_set) + } + phx-disable-with={gettext("Saving...")} + variant="primary" + class="mt-2" + > + {gettext("Save OIDC Settings")} + + """ @@ -265,8 +361,12 @@ defmodule MvWeb.GlobalSettingsLive do @impl true def handle_event("save", %{"setting" => setting_params}, socket) do actor = MvWeb.LiveHelpers.current_actor(socket) - # Never send blank API key so we do not overwrite the stored secret (security) - setting_params_clean = drop_blank_vereinfacht_api_key(setting_params) + # Never send blank API key / client secret so we do not overwrite stored secrets + setting_params_clean = + setting_params + |> drop_blank_vereinfacht_api_key() + |> drop_blank_oidc_client_secret() + saves_vereinfacht = vereinfacht_params?(setting_params_clean) case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do @@ -280,6 +380,7 @@ defmodule MvWeb.GlobalSettingsLive do socket |> assign(:settings, fresh_settings) |> assign(:vereinfacht_api_key_set, present?(fresh_settings.vereinfacht_api_key)) + |> assign(:oidc_client_secret_set, present?(fresh_settings.oidc_client_secret)) |> assign(:vereinfacht_test_result, test_result) |> put_flash(:info, gettext("Settings updated successfully")) |> assign_form() @@ -307,88 +408,19 @@ defmodule MvWeb.GlobalSettingsLive do end end - @impl true - def handle_info({:custom_field_saved, _custom_field, action}, socket) do - send_update(MvWeb.CustomFieldLive.IndexComponent, - id: "custom-fields-component", - show_form: false - ) + defp drop_blank_oidc_client_secret(params) when is_map(params) do + case params do + %{"oidc_client_secret" => v} when v in [nil, ""] -> + Map.delete(params, "oidc_client_secret") - {:noreply, - socket - |> assign(:active_editing_section, nil) - |> put_flash(:info, gettext("Data field %{action} successfully", action: action))} - end - - @impl true - def handle_info({:custom_field_deleted, _custom_field}, socket) do - {:noreply, put_flash(socket, :info, gettext("Data field deleted successfully"))} - end - - @impl true - def handle_info({:custom_field_delete_error, error}, socket) do - {:noreply, - put_flash( - socket, - :error, - gettext("Failed to delete data field: %{error}", error: inspect(error)) - )} - end - - @impl true - def handle_info(:custom_field_slug_mismatch, socket) do - {:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))} - end - - def handle_info({:custom_fields_load_error, _error}, socket) do - {:noreply, - put_flash( - socket, - :error, - gettext("Could not load data fields. Please check your permissions.") - )} - end - - @impl true - def handle_info({:editing_section_changed, section}, socket) do - {:noreply, assign(socket, :active_editing_section, section)} - end - - @impl true - def handle_info({:member_field_saved, _member_field, action}, socket) do - # Reload settings to get updated member_field_visibility - {:ok, updated_settings} = Membership.get_settings() - - # Send update to member fields component to close form - send_update(MvWeb.MemberFieldLive.IndexComponent, - id: "member-fields-component", - show_form: false, - settings: updated_settings - ) - - {:noreply, - socket - |> assign(:settings, updated_settings) - |> assign(:active_editing_section, nil) - |> put_flash(:info, gettext("Member field %{action} successfully", action: action))} - end - - @impl true - def handle_info({:member_field_visibility_updated}, socket) do - # Legacy event - reload settings and update component - {:ok, updated_settings} = Membership.get_settings() - - send_update(MvWeb.MemberFieldLive.IndexComponent, - id: "member-fields-component", - settings: updated_settings - ) - - {:noreply, assign(socket, :settings, updated_settings)} + _ -> + params + end end defp assign_form(%{assigns: %{settings: settings}} = socket) do - # Never put API key into form/DOM to avoid secret leak in source or DevTools - settings_for_form = %{settings | vereinfacht_api_key: nil} + # Never put API key / client secret into form/DOM to avoid secret leak + settings_for_form = %{settings | vereinfacht_api_key: nil, oidc_client_secret: nil} form = AshPhoenix.Form.for_update( diff --git a/priv/repo/migrations/20260224122831_add_oidc_to_settings.exs b/priv/repo/migrations/20260224122831_add_oidc_to_settings.exs new file mode 100644 index 0000000..154709a --- /dev/null +++ b/priv/repo/migrations/20260224122831_add_oidc_to_settings.exs @@ -0,0 +1,29 @@ +defmodule Mv.Repo.Migrations.AddOidcToSettings do + @moduledoc """ + Adds OIDC configuration columns to settings (ENV-overridable in UI). + """ + + use Ecto.Migration + + def up do + alter table(:settings) do + add :oidc_client_id, :string + add :oidc_base_url, :string + add :oidc_redirect_uri, :string + add :oidc_client_secret, :string + add :oidc_admin_group_name, :string + add :oidc_groups_claim, :string + end + end + + def down do + alter table(:settings) do + remove :oidc_client_id + remove :oidc_base_url + remove :oidc_redirect_uri + remove :oidc_client_secret + remove :oidc_admin_group_name + remove :oidc_groups_claim + end + end +end diff --git a/priv/resource_snapshots/repo/settings/20260224122831.json b/priv/resource_snapshots/repo/settings/20260224122831.json new file mode 100644 index 0000000..b1ad095 --- /dev/null +++ b/priv/resource_snapshots/repo/settings/20260224122831.json @@ -0,0 +1,164 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "club_name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "member_field_visibility", + "type": "map" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "member_field_required", + "type": "map" + }, + { + "allow_nil?": false, + "default": "true", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "include_joining_cycle", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "default_membership_fee_type_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "vereinfacht_api_url", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "vereinfacht_api_key", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "vereinfacht_club_id", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "vereinfacht_app_url", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "create_table_options": null, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "C84FC81A2A446451D6B5EA72F9BBB3593CD7F0D71C4B7C9CE04934414FDB52EB", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "settings" +} \ No newline at end of file diff --git a/test/mv/oidc_role_sync_config_test.exs b/test/mv/oidc_role_sync_config_test.exs index b4664aa..4b77378 100644 --- a/test/mv/oidc_role_sync_config_test.exs +++ b/test/mv/oidc_role_sync_config_test.exs @@ -1,21 +1,22 @@ defmodule Mv.OidcRoleSyncConfigTest do @moduledoc """ Tests for OIDC role sync configuration (OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM). + Reads via Mv.Config (ENV first, then Settings). """ - use ExUnit.Case, async: false + use Mv.DataCase, async: false alias Mv.OidcRoleSyncConfig describe "oidc_admin_group_name/0" do test "returns nil when OIDC_ADMIN_GROUP_NAME is not configured" do - restore = put_config(admin_group_name: nil) + restore = clear_env("OIDC_ADMIN_GROUP_NAME") on_exit(restore) assert OidcRoleSyncConfig.oidc_admin_group_name() == nil end - test "returns configured admin group name when set" do - restore = put_config(admin_group_name: "mila-admin") + test "returns configured admin group name when set via ENV" do + restore = set_env("OIDC_ADMIN_GROUP_NAME", "mila-admin") on_exit(restore) assert OidcRoleSyncConfig.oidc_admin_group_name() == "mila-admin" @@ -24,26 +25,35 @@ defmodule Mv.OidcRoleSyncConfigTest do describe "oidc_groups_claim/0" do test "returns default \"groups\" when OIDC_GROUPS_CLAIM is not configured" do - restore = put_config(groups_claim: nil) + restore = clear_env("OIDC_GROUPS_CLAIM") on_exit(restore) assert OidcRoleSyncConfig.oidc_groups_claim() == "groups" end - test "returns configured claim name when OIDC_GROUPS_CLAIM is set" do - restore = put_config(groups_claim: "ak_groups") + test "returns configured claim name when OIDC_GROUPS_CLAIM is set via ENV" do + restore = set_env("OIDC_GROUPS_CLAIM", "ak_groups") on_exit(restore) assert OidcRoleSyncConfig.oidc_groups_claim() == "ak_groups" end end - defp put_config(opts) do - current = Application.get_env(:mv, :oidc_role_sync, []) - Application.put_env(:mv, :oidc_role_sync, Keyword.merge(current, opts)) + defp set_env(key, value) do + previous = System.get_env(key) + System.put_env(key, value) fn -> - Application.put_env(:mv, :oidc_role_sync, current) + if previous, do: System.put_env(key, previous), else: System.delete_env(key) + end + end + + defp clear_env(key) do + previous = System.get_env(key) + System.delete_env(key) + + fn -> + if previous, do: System.put_env(key, previous) end end end