Fix authorization bypass in seeds and validations
All checks were successful
continuous-integration/drone/push Build is passing

- Add authorize?: false to all bootstrap operations in seeds.exs
- Fix user-linking validation to respect authorize? context flag
- Prevents authorization errors during initial setup when no actor exists yet
This commit is contained in:
Moritz 2026-01-23 02:08:11 +01:00
parent 67b5d623cf
commit 079d270768
2 changed files with 47 additions and 17 deletions

View file

@ -407,8 +407,16 @@ defmodule Mv.Membership.Member do
actor = Map.get(changeset.context || %{}, :actor) actor = Map.get(changeset.context || %{}, :actor)
# Check the current state of the user in the database # Check the current state of the user in the database
# Pass actor to ensure proper authorization (User might have policies in future) # Check if authorization is disabled in the parent operation's context
case Ash.get(Mv.Accounts.User, user_id, actor: actor) do # Access private context where authorize? flag is stored
authorize? =
case get_in(changeset.context, [:private, :authorize?]) do
false -> false
_ -> true
end
# Pass actor and authorize? to ensure proper authorization (User might have policies in future)
case Ash.get(Mv.Accounts.User, user_id, actor: actor, authorize?: authorize?) do
# User is free to be linked # User is free to be linked
{:ok, %{member_id: nil}} -> {:ok, %{member_id: nil}} ->
:ok :ok

View file

@ -161,24 +161,30 @@ end
# This handles both existing users (e.g., from OIDC) and newly created users # This handles both existing users (e.g., from OIDC) and newly created users
case Accounts.User case Accounts.User
|> Ash.Query.filter(email == ^admin_email) |> Ash.Query.filter(email == ^admin_email)
|> Ash.read_one(domain: Mv.Accounts) do |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
{:ok, existing_admin_user} when not is_nil(existing_admin_user) -> {:ok, existing_admin_user} when not is_nil(existing_admin_user) ->
# User already exists (e.g., via OIDC) - assign admin role # User already exists (e.g., via OIDC) - assign admin role
# Use authorize?: false for bootstrap - this is initial setup
existing_admin_user existing_admin_user
|> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!() |> Ash.update!(authorize?: false)
{:ok, nil} -> {:ok, nil} ->
# User doesn't exist - create admin user with password # User doesn't exist - create admin user with password
Accounts.create_user!(%{email: admin_email}, upsert?: true, upsert_identity: :unique_email) # Use authorize?: false for bootstrap - no admin user exists yet to use as actor
Accounts.create_user!(%{email: admin_email},
upsert?: true,
upsert_identity: :unique_email,
authorize?: false
)
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|> Ash.update!() |> Ash.update!(authorize?: false)
|> then(fn user -> |> then(fn user ->
user user
|> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!() |> Ash.update!(authorize?: false)
end) end)
{:error, error} -> {:error, error} ->
@ -190,10 +196,10 @@ end
admin_user_with_role = admin_user_with_role =
case Accounts.User case Accounts.User
|> Ash.Query.filter(email == ^admin_email) |> Ash.Query.filter(email == ^admin_email)
|> Ash.read_one(domain: Mv.Accounts) do |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
{:ok, user} when not is_nil(user) -> {:ok, user} when not is_nil(user) ->
user user
|> Ash.load!(:role) |> Ash.load!(:role, authorize?: false)
{:ok, nil} -> {:ok, nil} ->
raise "Admin user not found after creation/assignment" raise "Admin user not found after creation/assignment"
@ -209,13 +215,14 @@ system_user_email = Mv.Helpers.SystemActor.system_user_email()
case Accounts.User case Accounts.User
|> Ash.Query.filter(email == ^system_user_email) |> Ash.Query.filter(email == ^system_user_email)
|> Ash.read_one(domain: Mv.Accounts) do |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
{:ok, existing_system_user} when not is_nil(existing_system_user) -> {:ok, existing_system_user} when not is_nil(existing_system_user) ->
# System user already exists - ensure it has admin role # System user already exists - ensure it has admin role
# Use authorize?: false for bootstrap
existing_system_user existing_system_user
|> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!() |> Ash.update!(authorize?: false)
{:ok, nil} -> {:ok, nil} ->
# System user doesn't exist - create it with admin role # System user doesn't exist - create it with admin role
@ -224,13 +231,15 @@ case Accounts.User
# - No OIDC ID (oidc_id = nil) - prevents OIDC login # - No OIDC ID (oidc_id = nil) - prevents OIDC login
# - This user is ONLY for internal system operations via SystemActor # - This user is ONLY for internal system operations via SystemActor
# If either hashed_password or oidc_id is set, the user could potentially log in # If either hashed_password or oidc_id is set, the user could potentially log in
# Use authorize?: false for bootstrap - system user creation happens before system actor exists
Accounts.create_user!(%{email: system_user_email}, Accounts.create_user!(%{email: system_user_email},
upsert?: true, upsert?: true,
upsert_identity: :unique_email upsert_identity: :unique_email,
authorize?: false
) )
|> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!() |> Ash.update!(authorize?: false)
{:error, error} -> {:error, error} ->
# Log error but don't fail seeds - SystemActor will fall back to admin user # Log error but don't fail seeds - SystemActor will fall back to admin user
@ -397,9 +406,20 @@ additional_users = [
created_users = created_users =
Enum.map(additional_users, fn user_attrs -> Enum.map(additional_users, fn user_attrs ->
Accounts.create_user!(user_attrs, upsert?: true, upsert_identity: :unique_email) # Use admin user as actor for additional user creation (not bootstrap)
user =
Accounts.create_user!(user_attrs,
upsert?: true,
upsert_identity: :unique_email,
actor: admin_user_with_role
)
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|> Ash.update!() |> Ash.update!(actor: admin_user_with_role)
# Reload user to ensure all fields (including member_id) are loaded
Accounts.User
|> Ash.Query.filter(id == ^user.id)
|> Ash.read_one!(domain: Mv.Accounts, actor: admin_user_with_role)
end) end)
# Create members with linked users to demonstrate the 1:1 relationship # Create members with linked users to demonstrate the 1:1 relationship
@ -449,11 +469,13 @@ Enum.with_index(linked_members)
member = member =
if user.member_id == nil do if user.member_id == nil do
# User is free, create member and link - use upsert to prevent duplicates # User is free, create member and link - use upsert to prevent duplicates
# Use authorize?: false for User lookup during relationship management (bootstrap phase)
Membership.create_member!( Membership.create_member!(
Map.put(member_attrs_without_fee_type, :user, %{id: user.id}), Map.put(member_attrs_without_fee_type, :user, %{id: user.id}),
upsert?: true, upsert?: true,
upsert_identity: :unique_email, upsert_identity: :unique_email,
actor: admin_user_with_role actor: admin_user_with_role,
authorize?: false
) )
else else
# User already has a member, just create the member without linking - use upsert to prevent duplicates # User already has a member, just create the member without linking - use upsert to prevent duplicates