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)
# Check the current state of the user in the database
# Pass actor to ensure proper authorization (User might have policies in future)
case Ash.get(Mv.Accounts.User, user_id, actor: actor) do
# Check if authorization is disabled in the parent operation's context
# 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
{:ok, %{member_id: nil}} ->
:ok

View file

@ -161,24 +161,30 @@ end
# This handles both existing users (e.g., from OIDC) and newly created users
case Accounts.User
|> 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) ->
# User already exists (e.g., via OIDC) - assign admin role
# Use authorize?: false for bootstrap - this is initial setup
existing_admin_user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!()
|> Ash.update!(authorize?: false)
{:ok, nil} ->
# 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.update!()
|> Ash.update!(authorize?: false)
|> then(fn user ->
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!()
|> Ash.update!(authorize?: false)
end)
{:error, error} ->
@ -190,10 +196,10 @@ end
admin_user_with_role =
case Accounts.User
|> 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) ->
user
|> Ash.load!(:role)
|> Ash.load!(:role, authorize?: false)
{:ok, nil} ->
raise "Admin user not found after creation/assignment"
@ -209,13 +215,14 @@ system_user_email = Mv.Helpers.SystemActor.system_user_email()
case Accounts.User
|> 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) ->
# System user already exists - ensure it has admin role
# Use authorize?: false for bootstrap
existing_system_user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!()
|> Ash.update!(authorize?: false)
{:ok, nil} ->
# 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
# - 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
# Use authorize?: false for bootstrap - system user creation happens before system actor exists
Accounts.create_user!(%{email: system_user_email},
upsert?: true,
upsert_identity: :unique_email
upsert_identity: :unique_email,
authorize?: false
)
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!()
|> Ash.update!(authorize?: false)
{:error, error} ->
# Log error but don't fail seeds - SystemActor will fall back to admin user
@ -397,9 +406,20 @@ additional_users = [
created_users =
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.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)
# Create members with linked users to demonstrate the 1:1 relationship
@ -449,11 +469,13 @@ Enum.with_index(linked_members)
member =
if user.member_id == nil do
# 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!(
Map.put(member_attrs_without_fee_type, :user, %{id: user.id}),
upsert?: true,
upsert_identity: :unique_email,
actor: admin_user_with_role
actor: admin_user_with_role,
authorize?: false
)
else
# User already has a member, just create the member without linking - use upsert to prevent duplicates