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.
547 lines
18 KiB
Ruby
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
|