Files
ubicloud/spec/routes/web/auth_spec.rb
Jeremy Evans 096ac0111b Switch access control to use AccessControlEntry and {Subject,Action,Object}Tag
Previously, access control was implemented via AccessPolicy and {Access/AppliedTag}.
This doesn't remove AccessPolicy/AppliedTag yet, as there are still references to
them.  They will be removed in a later commit.

In order for the new access control to work, the authorization methods need to be
passed the project_id, to force the access query to operate on a single project.
That part isn't very invasive, since authorization is mostly centralized in
helpers/general.rb, but it did require a couple other places to be changed:

* model/invoice.rb is the only caller of Authorization.authorize outside of
  the Clover helpers.  This is probably a layer violation that should be fixed
  later, but for now, add the project_id manually to the call.

* In the github route, set @project so that the helpers can access it.

Add Project#dissociate_subject, which handles remove the subject tags and
direct access control entries when removing a user or token from a project.
Call this when removing uses or tokens from a project.

Remove Authorization.authorized_resources_dataset, as it was only
needed by Dataset#authorize.  This inlines the code into Dataset#authorize.
This was done because it doesn't make sense to use the method in isolation,
and the query needed in Dataset#authorize depends on the dataset, which
isn't passed to Authorization.authorized_resources_dataset.

Remove Authorization.expand_actions.  This method has no equivalent in the
new design, and it is no longer needed.

For the api specs that use a personal access token, this adds the
token to the Admin subject tag (which has full access).  This has
similar behavior as applying the admin managed policy had previously.

The authorization queries do not currently support nested tags.  Support
for nested tags will be added later.

While here, fix parameter name for Clover#all_permissions.
This method is only currently called with @project.id as the argument,
so it doesn't technically need an argument, but it could potentially
be used later to get all permissions on an object other than the
current project.
2025-01-09 09:55:55 -08:00

547 lines
18 KiB
Ruby

# frozen_string_literal: true
require_relative "spec_helper"
RSpec.describe Clover, "auth" do
it "redirects root to login" do
visit "/"
expect(page).to have_current_path("/login")
end
it "can not login new account without verification" do
visit "/create-account"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Full Name", with: "John Doe"
fill_in "Password", with: TEST_USER_PASSWORD
fill_in "Password Confirmation", with: TEST_USER_PASSWORD
click_button "Create Account"
expect(Mail::TestMailer.deliveries.length).to eq 1
expect(page.title).to eq("Ubicloud - Login")
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page.title).to eq("Ubicloud - Resend Verification")
end
it "can not create new account with invalid name" do
visit "/create-account"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Full Name", with: "Click here http://example.com"
fill_in "Password", with: TEST_USER_PASSWORD
fill_in "Password Confirmation", with: TEST_USER_PASSWORD
click_button "Create Account"
expect(page.title).to eq("Ubicloud - Create Account")
expect(Mail::TestMailer.deliveries.length).to eq 0
expect(page).to have_content("Name must only contain letters, numbers, spaces, and hyphens and have max length 63.")
end
it "can send email verification email again after 300 seconds" do
visit "/create-account"
fill_in "Full Name", with: "John Doe"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
fill_in "Password Confirmation", with: TEST_USER_PASSWORD
click_button "Create Account"
expect(page).to have_flash_notice("An email has been sent to you with a link to verify your account")
expect(Mail::TestMailer.deliveries.length).to eq 1
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page).to have_content("You need to wait at least 300 seconds before sending another verification email. If you did not receive the email, please check your spam folder.")
DB[:account_verification_keys].update(email_last_sent: Time.now - 310)
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page).to have_flash_error("The account you tried to login with is currently awaiting verification")
click_button "Send Verification Again"
expect(page).to have_flash_notice("An email has been sent to you with a link to verify your account")
expect(Mail::TestMailer.deliveries.length).to eq 2
end
it "can create new account, verify it, and visit project which invited" do
p = Project.create_with_id(name: "Invited-project").tap { _1.associate_with_project(_1) }
p.add_invitation(email: TEST_USER_EMAIL, inviter_id: "bd3479c6-5ee3-894c-8694-5190b76f84cf", expires_at: Time.now + 7 * 24 * 60 * 60)
visit "/create-account"
fill_in "Full Name", with: "John Doe"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
fill_in "Password Confirmation", with: TEST_USER_PASSWORD
click_button "Create Account"
expect(page).to have_flash_notice("An email has been sent to you with a link to verify your account")
expect(Mail::TestMailer.deliveries.length).to eq 1
verify_link = Mail::TestMailer.deliveries.first.html_part.body.match(/(\/verify-account.+?)"/)[1]
visit verify_link
expect(page.title).to eq("Ubicloud - Verify Account")
click_button "Verify Account"
expect(page.title).to eq("Ubicloud - Default Dashboard")
visit "#{p.path}/dashboard"
expect(page.title).to eq("Ubicloud - #{p.name} Dashboard")
end
it "can create new account, verify it, and visit project which invited with default policy" do
p = Project.create_with_id(name: "Invited-project").tap { _1.associate_with_project(_1) }
subject_id = SubjectTag.create_with_id(project_id: p.id, name: "Admin").id
AccessControlEntry.create_with_id(project_id: p.id, subject_id:, action_id: ActionType::NAME_MAP["Project:view"])
p.add_invitation(email: TEST_USER_EMAIL, policy: "Admin", inviter_id: "bd3479c6-5ee3-894c-8694-5190b76f84cf", expires_at: Time.now + 7 * 24 * 60 * 60)
visit "/create-account"
fill_in "Full Name", with: "John Doe"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
fill_in "Password Confirmation", with: TEST_USER_PASSWORD
click_button "Create Account"
expect(page).to have_flash_notice("An email has been sent to you with a link to verify your account")
expect(Mail::TestMailer.deliveries.length).to eq 1
verify_link = Mail::TestMailer.deliveries.first.html_part.body.match(/(\/verify-account.+?)"/)[1]
visit verify_link
expect(page.title).to eq("Ubicloud - Verify Account")
click_button "Verify Account"
expect(page.title).to eq("Ubicloud - Default Dashboard")
visit p.path
expect(page.title).to eq("Ubicloud - #{p.name}")
end
it "can remember login" do
account = create_account
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
check "Remember me"
click_button "Sign in"
expect(page.title).to eq("Ubicloud - #{account.projects.first.name} Dashboard")
expect(DB[:account_remember_keys].first(id: account.id)).not_to be_nil
end
it "has correct current user when logged in via remember token" do
account = create_account
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
check "Remember me"
click_button "Sign in"
expect(page.title).to eq("Ubicloud - #{account.projects.first.name} Dashboard")
page.driver.browser.rack_mock_session.cookie_jar.delete("_Clover.session")
page.refresh
expect(page.title).to eq("Ubicloud - #{account.projects.first.name} Dashboard")
end
it "can reset password" do
create_account
visit "/login"
click_link "Forgot your password?"
fill_in "Email Address", with: TEST_USER_EMAIL
click_button "Request Password Reset"
expect(page).to have_flash_notice("An email has been sent to you with a link to reset the password for your account")
expect(Mail::TestMailer.deliveries.length).to eq 1
reset_link = Mail::TestMailer.deliveries.first.html_part.body.match(/(\/reset-password.+?)"/)[1]
visit reset_link
expect(page.title).to eq("Ubicloud - Reset Password")
fill_in "Password", with: "#{TEST_USER_PASSWORD}_new"
fill_in "Password Confirmation", with: "#{TEST_USER_PASSWORD}_new"
click_button "Reset Password"
expect(page.title).to eq("Ubicloud - Login")
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: "#{TEST_USER_PASSWORD}_new"
click_button "Sign in"
end
it "can not reset password if password disabled" do
account = create_account
DB[:account_password_hashes].where(id: account.id).delete
visit "/login"
click_link "Forgot your password?"
fill_in "Email Address", with: TEST_USER_EMAIL
click_button "Request Password Reset"
expect(page).to have_flash_error(/Login with password is not enabled for this account.*/)
expect(DB[:account_password_reset_keys].count).to eq 0
end
it "can login to an account without projects" do
create_account(with_project: false)
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page.title).to eq("Ubicloud - Projects")
end
it "can not login if the account is suspended" do
account = create_account
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page.title).to eq("Ubicloud - #{account.projects.first.name} Dashboard")
account.suspend
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page.title).to eq("Ubicloud - Login")
expect(page).to have_flash_error(/Your account has been suspended.*/)
end
it "can not login if the account is suspended via remember token" do
account = create_account
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
check "Remember me"
click_button "Sign in"
expect(page.title).to eq("Ubicloud - #{account.projects.first.name} Dashboard")
page.driver.browser.rack_mock_session.cookie_jar.delete("_Clover.session")
account.suspend
page.refresh
expect(page.title).to eq("Ubicloud - Login")
end
it "redirects to otp page if the otp is only 2FA method" do
create_account(enable_otp: true)
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page.title).to eq("Ubicloud - 2FA - One-Time Password")
end
it "redirects to webauthn page if the webauthn is only 2FA method" do
create_account(enable_webauthn: true)
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page.title).to eq("Ubicloud - 2FA - Security Keys")
end
it "shows 2FA method list if there are multiple 2FA methods" do
create_account(enable_otp: true, enable_webauthn: true)
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
expect(page.title).to eq("Ubicloud - Two-factor Authentication")
end
it "shows enter recovery codes page" do
create_account(enable_otp: true)
visit "/login"
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
click_link "Enter a recovery code"
expect(page.title).to eq("Ubicloud - 2FA - Recovery Codes")
end
describe "authenticated" do
before do
create_account
login
end
it "redirects root to dashboard" do
visit "/dashboard"
expect(page).to have_current_path("/dashboard")
end
it "can logout" do
visit "/dashboard"
click_button "Log out"
expect(page.title).to eq("Ubicloud - Login")
end
it "can change email" do
new_email = "new@example.com"
visit "/account/change-login"
fill_in "New Email Address", with: new_email
click_button "Change Email"
expect(page).to have_flash_notice("An email has been sent to you with a link to verify your login change")
expect(Mail::TestMailer.deliveries.length).to eq 1
verify_link = Mail::TestMailer.deliveries.first.html_part.body.match(/(\/verify-login-change.+?)"/)[1]
visit verify_link
expect(page.title).to eq("Ubicloud - Verify New Email")
click_button "Click to Verify New Email"
expect(page.title).to eq("Ubicloud - Default Dashboard")
click_button "Log out"
expect(page.title).to eq("Ubicloud - Login")
fill_in "Email Address", with: new_email
fill_in "Password", with: TEST_USER_PASSWORD
click_button "Sign in"
end
it "can change password" do
visit "/account/change-password"
fill_in "New Password", with: "#{TEST_USER_PASSWORD}_new"
fill_in "New Password Confirmation", with: "#{TEST_USER_PASSWORD}_new"
click_button "Change Password"
expect(page.title).to eq("Ubicloud - Change Password")
click_button "Log out"
expect(page.title).to eq("Ubicloud - Login")
fill_in "Email Address", with: TEST_USER_EMAIL
fill_in "Password", with: "#{TEST_USER_PASSWORD}_new"
click_button "Sign in"
end
it "can close account" do
account = Account[email: TEST_USER_EMAIL]
UsageAlert.create_with_id(project_id: account.projects.first.id, user_id: account.id, name: "test", limit: 100)
visit "/account/close-account"
click_button "Close Account"
expect(page.title).to eq("Ubicloud - Login")
expect(page).to have_flash_notice("Your account has been closed")
expect(Account[email: TEST_USER_EMAIL]).to be_nil
expect(AccessTag.where(name: "user/#{TEST_USER_EMAIL}").count).to eq 0
end
it "can not close account if the project has some resources" do
vm = create_vm
project = Account[email: TEST_USER_EMAIL].projects.first
vm.associate_with_project(project)
visit "/account/close-account"
click_button "Close Account"
expect(page.title).to eq("Ubicloud - Close Account")
expect(page).to have_flash_error("'#{project.name}' project has some resources. Delete all related resources first.")
end
end
describe "social login" do
def mock_provider(provider, email = TEST_USER_EMAIL)
expect(Config).to receive("omniauth_#{provider}_id").and_return("12345").at_least(:once)
OmniAuth.config.add_mock(provider, {
provider: provider,
uid: "123456790",
info: {
name: "John Doe",
email: email
}
})
end
before do
OmniAuth.config.logger = Logger.new(IO::NULL)
OmniAuth.config.test_mode = true
end
it "can create new account" do
mock_provider(:github)
visit "/login"
click_button "GitHub"
account = Account[email: TEST_USER_EMAIL]
expect(account).not_to be_nil
expect(account.identities_dataset.first(provider: "github", uid: "123456790")).not_to be_nil
expect(page.status_code).to eq(200)
expect(page.title).to eq("Ubicloud - #{account.projects.first.name} Dashboard")
end
it "can login existing account" do
mock_provider(:google)
account = create_account
account.add_identity(provider: "google", uid: "123456790")
visit "/login"
click_button "Google"
expect(Account.count).to eq(1)
expect(AccountIdentity.count).to eq(1)
expect(page.status_code).to eq(200)
expect(page.title).to eq("Ubicloud - #{account.projects.first.name} Dashboard")
end
it "can not login existing account before linking it" do
mock_provider(:github)
create_account
visit "/login"
click_button "GitHub"
expect(page.status_code).to eq(200)
expect(page.title).to eq("Ubicloud - Login")
expect(page).to have_flash_error(/There is already an account with this email address.*/)
end
describe "authenticated" do
let(:account) { create_account }
before do
login(account.email)
end
it "can connect to existing account" do
mock_provider(:github, "uSer@example.com")
visit "/account/login-method"
within "#login-method-github" do
click_button "Connect"
end
expect(page.title).to eq("Ubicloud - Login Methods")
expect(page).to have_flash_notice("You have successfully connected your account with Github.")
end
it "can disconnect from existing account" do
account.add_identity(provider: "google", uid: "123456790")
account.add_identity(provider: "github", uid: "123456790")
visit "/account/login-method"
within "#login-method-github" do
click_button "Disconnect"
end
expect(page.title).to eq("Ubicloud - Login Methods")
expect(page).to have_flash_notice("Your account has been disconnected from Github")
end
it "can delete password if another login method is available" do
account.add_identity(provider: "google", uid: "123456790")
visit "/account/login-method"
within "#login-method-password" do
click_button "Delete"
end
expect(page.title).to eq("Ubicloud - Login Methods")
expect(page).to have_flash_notice("Your password has been deleted")
end
it "can not disconnect the last login method if has no password" do
DB[:account_password_hashes].where(id: account.id).delete
account.add_identity(provider: "github", uid: "123456790")
visit "/account/login-method"
within "#login-method-github" do
click_button "Disconnect"
end
expect(page.title).to eq("Ubicloud - Login Methods")
expect(page).to have_flash_error("You must have at least one login method")
end
it "can not disconnect if it's already disconnected" do
account.add_identity(provider: "google", uid: "123456790")
account.add_identity(provider: "github", uid: "123456790")
visit "/account/login-method"
account.identities_dataset.first(provider: "github").update(uid: "0987654321")
within "#login-method-github" do
click_button "Disconnect"
end
expect(page.title).to eq("Ubicloud - Login Methods")
expect(page).to have_flash_error("Your account already has been disconnected from Github")
end
it "can not connect an account with different email" do
mock_provider(:github, "user2@example.com")
visit "/account/login-method"
within "#login-method-github" do
click_button "Connect"
end
expect(page.title).to eq("Ubicloud - Login Methods")
expect(page).to have_flash_error("Your account's email address is different from the email address associated with the Github account.")
end
it "can not connect a social account with multiple accounts" do
create_account("user2@example.com")
mock_provider(:github, "user2@example.com")
visit "/account/login-method"
within "#login-method-github" do
click_button "Connect"
end
expect(page.title).to eq("Ubicloud - Login Methods")
expect(page).to have_flash_error("Your account's email address is different from the email address associated with the Github account.")
end
end
end
end