mirror of
https://github.com/we-promise/sure.git
synced 2026-07-16 14:55:23 +00:00
* fix: share family accounts with invited members on every sign-up path A user who joined an existing family through an invitation was added to the family but saw none of its accounts, even when the family's default sharing is "share with all members". Only Invitation#accept_for created the AccountShare records; the OIDC just-in-time sign-up, invite-token registration, and mobile SSO onboarding paths all skipped it, so invitees landed in the family with an empty account list. Signups routed into an invite-only default family had the same gap. Extract the sharing into Family#auto_share_existing_accounts_with, the single entry point for "a member just joined, apply the family's sharing policy". It honors default_account_sharing, only shares with a persisted member of the family, grants read_only to guests and read_write to everyone else, excludes accounts the user already owns, and is idempotent. accept_for and every sign-up path call it, so no current or future join path can reintroduce the empty-account bug or share accounts across families. Also wrap the two SSO account-creation paths (OIDC JIT and mobile SSO) in a transaction covering the user save, invitation acceptance, account sharing, and identity creation, so a failure partway through can no longer leave a half-onboarded user with no linked identity. * address review: guest read_only symmetric in auto_share_with_family!, load-bearing guard comment, mobile SSO private-no-op test
315 lines
9.7 KiB
Ruby
315 lines
9.7 KiB
Ruby
require "test_helper"
|
|
|
|
class OidcAccountsControllerTest < ActionController::TestCase
|
|
setup do
|
|
@user = users(:family_admin)
|
|
end
|
|
|
|
def pending_auth
|
|
{
|
|
"provider" => "openid_connect",
|
|
"uid" => "new-uid-12345",
|
|
"email" => @user.email,
|
|
"name" => "Bob Dylan",
|
|
"first_name" => "Bob",
|
|
"last_name" => "Dylan"
|
|
}
|
|
end
|
|
|
|
test "should show link page when pending auth exists" do
|
|
session[:pending_oidc_auth] = pending_auth
|
|
get :link
|
|
assert_response :success
|
|
end
|
|
|
|
test "should redirect to login when no pending auth" do
|
|
get :link
|
|
assert_redirected_to new_session_path
|
|
assert_equal "No pending OIDC authentication found", flash[:alert]
|
|
end
|
|
|
|
test "should create OIDC identity with valid password" do
|
|
session[:pending_oidc_auth] = pending_auth
|
|
|
|
assert_difference "OidcIdentity.count", 1 do
|
|
post :create_link,
|
|
params: {
|
|
email: @user.email,
|
|
password: user_password_test
|
|
}
|
|
end
|
|
|
|
assert_redirected_to root_path
|
|
assert_not_nil @user.oidc_identities.find_by(
|
|
provider: pending_auth["provider"],
|
|
uid: pending_auth["uid"]
|
|
)
|
|
end
|
|
|
|
test "should reject linking with invalid password" do
|
|
session[:pending_oidc_auth] = pending_auth
|
|
|
|
assert_no_difference "OidcIdentity.count" do
|
|
post :create_link,
|
|
params: {
|
|
email: @user.email,
|
|
password: "wrongpassword"
|
|
}
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
assert_equal "Invalid email or password", flash[:alert]
|
|
end
|
|
|
|
test "should redirect to MFA when user has MFA enabled" do
|
|
@user.setup_mfa!
|
|
@user.enable_mfa!
|
|
|
|
session[:pending_oidc_auth] = pending_auth
|
|
|
|
post :create_link,
|
|
params: {
|
|
email: @user.email,
|
|
password: user_password_test
|
|
}
|
|
|
|
assert_redirected_to verify_mfa_path
|
|
end
|
|
|
|
test "should reject create_link when no pending auth" do
|
|
post :create_link, params: {
|
|
email: @user.email,
|
|
password: user_password_test
|
|
}
|
|
|
|
assert_redirected_to new_session_path
|
|
assert_equal "No pending OIDC authentication found", flash[:alert]
|
|
end
|
|
|
|
# New user registration tests
|
|
def new_user_auth
|
|
{
|
|
"provider" => "openid_connect",
|
|
"uid" => "new-uid-99999",
|
|
"email" => "newuser@example.com",
|
|
"name" => "New User",
|
|
"first_name" => "New",
|
|
"last_name" => "User"
|
|
}
|
|
end
|
|
|
|
test "should show new_user page when pending auth exists" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
get :new_user
|
|
assert_response :success
|
|
end
|
|
|
|
test "should redirect new_user to login when no pending auth" do
|
|
get :new_user
|
|
assert_redirected_to new_session_path
|
|
assert_equal "No pending OIDC authentication found", flash[:alert]
|
|
end
|
|
|
|
test "should show create account option for new user" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
get :link
|
|
assert_response :success
|
|
assert_select "p", text: /Create New Account/
|
|
assert_select "strong", text: new_user_auth["email"]
|
|
end
|
|
|
|
test "does not show create account button when JIT link-only mode" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
AuthConfig.stubs(:jit_link_only?).returns(true)
|
|
AuthConfig.stubs(:allowed_oidc_domain?).returns(true)
|
|
|
|
get :link
|
|
assert_response :success
|
|
|
|
assert_select "p", text: /Create New Account/
|
|
# No create account button rendered
|
|
assert_select "button", text: "Create Account", count: 0
|
|
assert_select "p", text: /New account creation via single sign-on is disabled/
|
|
end
|
|
|
|
test "create_user redirects when JIT link-only mode" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
AuthConfig.stubs(:jit_link_only?).returns(true)
|
|
AuthConfig.stubs(:allowed_oidc_domain?).returns(true)
|
|
|
|
assert_no_difference [ "User.count", "OidcIdentity.count", "Family.count" ] do
|
|
post :create_user
|
|
end
|
|
|
|
assert_redirected_to new_session_path
|
|
assert_equal "SSO account creation is disabled. Please contact an administrator.", flash[:alert]
|
|
end
|
|
|
|
test "create_user redirects when email domain not allowed" do
|
|
disallowed_auth = new_user_auth.merge("email" => "newuser@notallowed.com")
|
|
session[:pending_oidc_auth] = disallowed_auth
|
|
|
|
AuthConfig.stubs(:jit_link_only?).returns(false)
|
|
AuthConfig.stubs(:allowed_oidc_domain?).with(disallowed_auth["email"]).returns(false)
|
|
|
|
assert_no_difference [ "User.count", "OidcIdentity.count", "Family.count" ] do
|
|
post :create_user
|
|
end
|
|
|
|
assert_redirected_to new_session_path
|
|
assert_equal "SSO account creation is disabled. Please contact an administrator.", flash[:alert]
|
|
end
|
|
|
|
test "should create new user account via OIDC" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
assert_difference [ "User.count", "OidcIdentity.count", "Family.count" ], 1 do
|
|
post :create_user
|
|
end
|
|
|
|
assert_redirected_to root_path
|
|
assert_equal "Welcome! Your account has been created.", flash[:notice]
|
|
|
|
# Verify user was created with correct details
|
|
new_user = User.find_by(email: new_user_auth["email"])
|
|
assert_not_nil new_user
|
|
assert_equal new_user_auth["first_name"], new_user.first_name
|
|
assert_equal new_user_auth["last_name"], new_user.last_name
|
|
assert_equal "admin", new_user.role # Family creators should be admin
|
|
|
|
# Verify OIDC identity was created
|
|
oidc_identity = new_user.oidc_identities.first
|
|
assert_not_nil oidc_identity
|
|
assert_equal new_user_auth["provider"], oidc_identity.provider
|
|
assert_equal new_user_auth["uid"], oidc_identity.uid
|
|
end
|
|
|
|
test "create_user uses form params for name when provided" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
assert_difference [ "User.count", "OidcIdentity.count" ], 1 do
|
|
post :create_user, params: {
|
|
user: { first_name: "Custom", last_name: "Name" }
|
|
}
|
|
end
|
|
|
|
assert_redirected_to root_path
|
|
|
|
new_user = User.find_by(email: new_user_auth["email"])
|
|
assert_equal "Custom", new_user.first_name
|
|
assert_equal "Name", new_user.last_name
|
|
end
|
|
|
|
test "create_user falls back to OIDC data when form params are blank" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
post :create_user, params: {
|
|
user: { first_name: "", last_name: "" }
|
|
}
|
|
|
|
new_user = User.find_by(email: new_user_auth["email"])
|
|
assert_equal new_user_auth["first_name"], new_user.first_name
|
|
assert_equal new_user_auth["last_name"], new_user.last_name
|
|
end
|
|
|
|
test "create_user rolls back user when OIDC identity creation fails" do
|
|
auth = new_user_auth.merge(
|
|
"email" => "oidc-rollback@example.com",
|
|
"uid" => "oidc-rollback-uid"
|
|
)
|
|
session[:pending_oidc_auth] = auth
|
|
OidcIdentity.stubs(:create_from_omniauth).raises(ActiveRecord::RecordNotUnique, "duplicate identity")
|
|
|
|
assert_no_difference [ "User.count", "OidcIdentity.count", "Family.count" ] do
|
|
post :create_user
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
assert_nil User.find_by(email: auth["email"])
|
|
end
|
|
|
|
test "should create session after OIDC registration" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
post :create_user
|
|
|
|
# Verify session was created
|
|
new_user = User.find_by(email: new_user_auth["email"])
|
|
assert Session.exists?(user_id: new_user.id)
|
|
end
|
|
|
|
test "should reject create_user when no pending auth" do
|
|
post :create_user
|
|
|
|
assert_redirected_to new_session_path
|
|
assert_equal "No pending OIDC authentication found", flash[:alert]
|
|
end
|
|
|
|
# Security: JIT users should NOT have password_digest set
|
|
test "JIT user is created without password_digest to prevent chained auth attacks" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
post :create_user
|
|
|
|
new_user = User.find_by(email: new_user_auth["email"])
|
|
assert_not_nil new_user, "User should be created"
|
|
assert_nil new_user.password_digest, "JIT user should have nil password_digest"
|
|
assert new_user.sso_only?, "JIT user should be SSO-only"
|
|
end
|
|
|
|
test "JIT user cannot authenticate with local password" do
|
|
session[:pending_oidc_auth] = new_user_auth
|
|
|
|
post :create_user
|
|
|
|
new_user = User.find_by(email: new_user_auth["email"])
|
|
|
|
# Attempting to authenticate should return nil (no password set)
|
|
assert_nil User.authenticate_by(
|
|
email: new_user.email,
|
|
password: "anypassword"
|
|
), "SSO-only user should not authenticate with password"
|
|
end
|
|
|
|
test "create_user via invitation shares existing family accounts when family shares by default" do
|
|
family = families(:dylan_family)
|
|
family.update!(default_account_sharing: "shared")
|
|
family.invitations.create!(email: "invitee@example.com", role: "member", inviter: users(:family_admin))
|
|
|
|
session[:pending_oidc_auth] = {
|
|
"provider" => "openid_connect", "uid" => "invite-uid-1",
|
|
"email" => "invitee@example.com", "first_name" => "In", "last_name" => "Vitee"
|
|
}
|
|
|
|
post :create_user
|
|
assert_redirected_to root_path
|
|
|
|
invitee = User.find_by(email: "invitee@example.com")
|
|
assert_not_nil invitee
|
|
assert_equal family.id, invitee.family_id
|
|
assert_equal family.accounts.pluck(:id).sort,
|
|
AccountShare.where(user: invitee).pluck(:account_id).sort
|
|
end
|
|
|
|
test "create_user via invitation shares nothing when family sharing is private" do
|
|
family = families(:dylan_family)
|
|
family.update!(default_account_sharing: "private")
|
|
family.invitations.create!(email: "invitee2@example.com", role: "member", inviter: users(:family_admin))
|
|
|
|
session[:pending_oidc_auth] = {
|
|
"provider" => "openid_connect", "uid" => "invite-uid-2",
|
|
"email" => "invitee2@example.com", "first_name" => "In", "last_name" => "Vitee"
|
|
}
|
|
|
|
post :create_user
|
|
assert_redirected_to root_path
|
|
|
|
invitee = User.find_by(email: "invitee2@example.com")
|
|
assert_equal family.id, invitee.family_id
|
|
assert_equal 0, AccountShare.where(user: invitee).count
|
|
end
|
|
end
|