Add OidcRoleSync: apply Admin/Mitglied from OIDC groups
Register and sign-in call apply_admin_role_from_user_info; users in configured admin group get Admin role, others get Mitglied. Internal User action + bypass policy.
This commit is contained in:
parent
a6e35da0f7
commit
99722dee26
4 changed files with 302 additions and 1 deletions
22
lib/mv/authorization/checks/oidc_role_sync_context.ex
Normal file
22
lib/mv/authorization/checks/oidc_role_sync_context.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
defmodule Mv.Authorization.Checks.OidcRoleSyncContext do
|
||||
@moduledoc """
|
||||
Policy check: true when the action is being run from OIDC role sync (context.private.oidc_role_sync).
|
||||
|
||||
Used to allow the internal set_role_from_oidc_sync action when called by Mv.OidcRoleSync
|
||||
without an actor.
|
||||
"""
|
||||
use Ash.Policy.SimpleCheck
|
||||
|
||||
@impl true
|
||||
def describe(_opts), do: "called from OIDC role sync (context.private.oidc_role_sync)"
|
||||
|
||||
@impl true
|
||||
def match?(_actor, authorizer, _opts) do
|
||||
# Context from opts (e.g. Ash.update!(..., context: %{private: %{oidc_role_sync: true}}))
|
||||
context = Map.get(authorizer, :context) || %{}
|
||||
from_context = get_in(context, [:private, :oidc_role_sync]) == true
|
||||
# When update runs inside create's after_action, context may not be passed; use process dict.
|
||||
from_process = Process.get(:oidc_role_sync) == true
|
||||
from_context or from_process
|
||||
end
|
||||
end
|
||||
82
lib/mv/oidc_role_sync.ex
Normal file
82
lib/mv/oidc_role_sync.ex
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
defmodule Mv.OidcRoleSync do
|
||||
@moduledoc """
|
||||
Syncs user role from OIDC user_info (e.g. groups claim → Admin role).
|
||||
|
||||
Used after OIDC registration (register_with_rauthy) and on sign-in so that
|
||||
users in the configured admin group get the Admin role; others get Mitglied.
|
||||
Configure via OIDC_ADMIN_GROUP_NAME and OIDC_GROUPS_CLAIM (see OidcRoleSyncConfig).
|
||||
"""
|
||||
alias Mv.Accounts.User
|
||||
alias Mv.Authorization.Role
|
||||
alias Mv.OidcRoleSyncConfig
|
||||
|
||||
@doc """
|
||||
Applies Admin or Mitglied role to the user based on OIDC user_info (groups claim).
|
||||
|
||||
- If OIDC_ADMIN_GROUP_NAME is not configured: no-op, returns :ok without changing the user.
|
||||
- If user_info contains the configured admin group (under OIDC_GROUPS_CLAIM): assigns Admin role.
|
||||
- Otherwise: assigns Mitglied role (downgrade if user was Admin).
|
||||
|
||||
user_info is a map (e.g. from JWT claims) and may use string keys. Groups can be
|
||||
a list of strings or a single string.
|
||||
|
||||
## Examples
|
||||
|
||||
user_info = %{"groups" => ["mila-admin"]}
|
||||
OidcRoleSync.apply_admin_role_from_user_info(user, user_info)
|
||||
|
||||
user_info = %{"ak_groups" => ["other"]} # with OIDC_GROUPS_CLAIM=ak_groups
|
||||
OidcRoleSync.apply_admin_role_from_user_info(user, user_info)
|
||||
"""
|
||||
@spec apply_admin_role_from_user_info(User.t(), map()) :: :ok
|
||||
def apply_admin_role_from_user_info(user, user_info) when is_map(user_info) do
|
||||
admin_group = OidcRoleSyncConfig.oidc_admin_group_name()
|
||||
|
||||
if is_nil(admin_group) or admin_group == "" do
|
||||
:ok
|
||||
else
|
||||
claim = OidcRoleSyncConfig.oidc_groups_claim()
|
||||
groups = groups_from_user_info(user_info, claim)
|
||||
target_role = if admin_group in groups, do: :admin, else: :mitglied
|
||||
set_user_role(user, target_role)
|
||||
end
|
||||
end
|
||||
|
||||
defp groups_from_user_info(user_info, claim) do
|
||||
case user_info[claim] do
|
||||
nil -> []
|
||||
list when is_list(list) -> Enum.map(list, &to_string/1)
|
||||
single when is_binary(single) -> [single]
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp set_user_role(user, :admin) do
|
||||
case Role.get_admin_role() do
|
||||
{:ok, %Role{} = role} ->
|
||||
do_set_role(user, role)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp set_user_role(user, :mitglied) do
|
||||
case Role.get_mitglied_role() do
|
||||
{:ok, %Role{} = role} ->
|
||||
do_set_role(user, role)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp do_set_role(user, role) do
|
||||
user
|
||||
|> Ash.Changeset.for_update(:set_role_from_oidc_sync, %{role_id: role.id})
|
||||
|> Ash.Changeset.set_context(%{private: %{oidc_role_sync: true}})
|
||||
|> Ash.update!(domain: Mv.Accounts, context: %{private: %{oidc_role_sync: true}})
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue