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
This commit is contained in:
parent
f29bbb02a2
commit
8edbbac95f
8 changed files with 487 additions and 136 deletions
|
|
@ -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 %>
|
||||
</.form>
|
||||
</.form_section>
|
||||
<%!-- 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 %>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
{gettext("Some values are set via environment variables. Those fields are read-only.")}
|
||||
</p>
|
||||
<% end %>
|
||||
<.form for={@form} id="oidc-form" phx-change="validate" phx-submit="save">
|
||||
<div class="grid gap-4">
|
||||
<.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"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div class="form-control">
|
||||
<label class="label" for={@form[:oidc_client_secret].id}>
|
||||
<span class="label-text">{gettext("Client Secret")}</span>
|
||||
<%= if @oidc_client_secret_set do %>
|
||||
<span class="label-text-alt badge badge-ghost">{gettext("(set)")}</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<.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
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<.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"
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<.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")}
|
||||
</.button>
|
||||
</.form>
|
||||
</.form_section>
|
||||
</Layouts.app>
|
||||
"""
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue