Init an admin user in prod closes #381 #409

Merged
moritz merged 14 commits from feature/381_init_admin into main 2026-02-04 20:53:02 +01:00
4 changed files with 80 additions and 8 deletions
Showing only changes of commit d573a22769 - Show all commits

View file

@ -118,6 +118,10 @@ defmodule Mv.Accounts.UserAuthenticationTest do
) )
case result do case result do
{:ok, found_user} when is_struct(found_user) ->
assert found_user.id == user.id
assert found_user.oidc_id == "oidc_identifier_12345"
{:ok, [found_user]} -> {:ok, [found_user]} ->
assert found_user.id == user.id assert found_user.id == user.id
assert found_user.oidc_id == "oidc_identifier_12345" assert found_user.oidc_id == "oidc_identifier_12345"
@ -125,6 +129,9 @@ defmodule Mv.Accounts.UserAuthenticationTest do
{:ok, []} -> {:ok, []} ->
flunk("User should be found by oidc_id") flunk("User should be found by oidc_id")
{:ok, nil} ->
flunk("User should be found by oidc_id")
{:error, error} -> {:error, error} ->
flunk("Unexpected error: #{inspect(error)}") flunk("Unexpected error: #{inspect(error)}")
end end
@ -219,11 +226,14 @@ defmodule Mv.Accounts.UserAuthenticationTest do
actor: system_actor actor: system_actor
) )
# Either returns empty list OR authentication error - both mean "user not found" # Either returns empty/nil OR authentication error - both mean "user not found"
case result do case result do
{:ok, []} -> {:ok, []} ->
:ok :ok
{:ok, nil} ->
:ok
{:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} -> {:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} ->
:ok :ok
@ -260,11 +270,14 @@ defmodule Mv.Accounts.UserAuthenticationTest do
actor: system_actor actor: system_actor
) )
# Either returns empty list OR authentication error - both mean "user not found" # Either returns empty/nil OR authentication error - both mean "user not found"
case result do case result do
{:ok, []} -> {:ok, []} ->
:ok :ok
{:ok, nil} ->
:ok
{:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} -> {:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} ->
:ok :ok

View file

@ -83,6 +83,25 @@ defmodule Mv.OidcRoleSyncTest do
{:ok, after_user} = get_user(user1.id) {:ok, after_user} = get_user(user1.id)
assert after_user.role_id == mitglied_role_id() assert after_user.role_id == mitglied_role_id()
end end
test "when user_info has no groups, groups are read from access_token JWT (e.g. Rauthy)" do
email = "sync-from-token-#{System.unique_integer([:positive])}@test.example.com"
{:ok, user} = create_user_with_mitglied(email)
user_info = %{"sub" => "oidc-123"}
# Minimal JWT: header.payload.signature with "groups" in payload (Rauthy puts groups in access_token)
payload = Jason.encode!(%{"groups" => ["mila-admin"], "sub" => "oidc-123"})
payload_b64 = Base.url_encode64(payload, padding: false)
header_b64 = Base.url_encode64("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", padding: false)
sig_b64 = Base.url_encode64("sig", padding: false)
access_token = "#{header_b64}.#{payload_b64}.#{sig_b64}"
oauth_tokens = %{"access_token" => access_token}
assert :ok = OidcRoleSync.apply_admin_role_from_user_info(user, user_info, oauth_tokens)
{:ok, after_user} = get_user(user.id)
assert after_user.role_id == admin_role_id()
end
end end
# B3: Role sync after registration is implemented via after_action in register_with_rauthy. # B3: Role sync after registration is implemented via after_action in register_with_rauthy.

View file

@ -37,7 +37,7 @@ defmodule MvWeb.OidcE2EFlowTest do
assert is_nil(new_user.hashed_password) assert is_nil(new_user.hashed_password)
# Verify user can be found by oidc_id # Verify user can be found by oidc_id
{:ok, [found_user]} = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_rauthy(
%{ %{
user_info: user_info, user_info: user_info,
@ -46,6 +46,13 @@ defmodule MvWeb.OidcE2EFlowTest do
actor: actor actor: actor
) )
found_user =
case result do
{:ok, u} when is_struct(u) -> u
{:ok, [u]} -> u
_ -> flunk("Expected user, got: #{inspect(result)}")
end
assert found_user.id == new_user.id assert found_user.id == new_user.id
end end
end end
@ -177,7 +184,7 @@ defmodule MvWeb.OidcE2EFlowTest do
assert linked_user.hashed_password == password_user.hashed_password assert linked_user.hashed_password == password_user.hashed_password
# Step 5: User can now sign in via OIDC # Step 5: User can now sign in via OIDC
{:ok, [signed_in_user]} = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_rauthy(
%{ %{
user_info: user_info, user_info: user_info,
@ -186,6 +193,13 @@ defmodule MvWeb.OidcE2EFlowTest do
actor: actor actor: actor
) )
signed_in_user =
case result do
{:ok, u} when is_struct(u) -> u
{:ok, [u]} -> u
_ -> flunk("Expected user, got: #{inspect(result)}")
end
assert signed_in_user.id == password_user.id assert signed_in_user.id == password_user.id
assert signed_in_user.oidc_id == "oidc_link_888" assert signed_in_user.oidc_id == "oidc_link_888"
end end
@ -331,6 +345,9 @@ defmodule MvWeb.OidcE2EFlowTest do
{:ok, []} -> {:ok, []} ->
:ok :ok
{:ok, nil} ->
:ok
{:error, %Ash.Error.Forbidden{}} -> {:error, %Ash.Error.Forbidden{}} ->
:ok :ok

View file

@ -27,7 +27,7 @@ defmodule MvWeb.OidcIntegrationTest do
# Test sign_in_with_rauthy action directly # Test sign_in_with_rauthy action directly
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, [found_user]} = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_rauthy(
%{ %{
user_info: user_info, user_info: user_info,
@ -36,6 +36,13 @@ defmodule MvWeb.OidcIntegrationTest do
actor: system_actor actor: system_actor
) )
found_user =
case result do
{:ok, u} when is_struct(u) -> u
{:ok, [u]} -> u
_ -> flunk("Expected user, got: #{inspect(result)}")
end
assert found_user.id == user.id assert found_user.id == user.id
assert to_string(found_user.email) == "existing@example.com" assert to_string(found_user.email) == "existing@example.com"
assert found_user.oidc_id == "existing_oidc_123" assert found_user.oidc_id == "existing_oidc_123"
@ -104,6 +111,9 @@ defmodule MvWeb.OidcIntegrationTest do
{:ok, []} -> {:ok, []} ->
:ok :ok
{:ok, nil} ->
:ok
{:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} -> {:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} ->
:ok :ok
@ -129,7 +139,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, [found_user]} = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_rauthy(
%{ %{
user_info: correct_user_info, user_info: correct_user_info,
@ -138,6 +148,13 @@ defmodule MvWeb.OidcIntegrationTest do
actor: system_actor actor: system_actor
) )
found_user =
case result do
{:ok, u} when is_struct(u) -> u
{:ok, [u]} -> u
_ -> flunk("Expected user, got: #{inspect(result)}")
end
assert found_user.id == user.id assert found_user.id == user.id
# Try with wrong oidc_id but correct email # Try with wrong oidc_id but correct email
@ -155,11 +172,14 @@ defmodule MvWeb.OidcIntegrationTest do
actor: system_actor actor: system_actor
) )
# Either returns empty list OR authentication error - both mean "user not found" # Either returns empty/nil OR authentication error - both mean "user not found"
case result do case result do
{:ok, []} -> {:ok, []} ->
:ok :ok
{:ok, nil} ->
:ok
{:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} -> {:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} ->
:ok :ok
@ -193,11 +213,14 @@ defmodule MvWeb.OidcIntegrationTest do
actor: system_actor actor: system_actor
) )
# Either returns empty list OR authentication error - both mean "user not found" # Either returns empty/nil OR authentication error - both mean "user not found"
case result do case result do
{:ok, []} -> {:ok, []} ->
:ok :ok
{:ok, nil} ->
:ok
{:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} -> {:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} ->
:ok :ok