403 leaks information about whether the requested project exists. This approach for projects is now similar to how we treat other nested objects, where we retrieve from an authorized dataset, instead of retrieving the object and then (hopefully) performing authorization on it. It should also be faster as it eliminates an unnecessary query. Unfortunatley, the route specs mock Project.[] in quite a few places. To avoid a bunch of spec churn, add Clover.authorized_project, and change the mocking to mock that instead. As a consequence of this handling, deleting an unauthorized project now returns 204 instead of 403. I believe that is how deletion of other unauthorized objects is handled, so the behavior is now more consistent, but it is something to be aware of.
180 lines
5.5 KiB
Ruby
180 lines
5.5 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
require_relative "spec_helper"
|
||
|
||
RSpec.describe Clover, "project" do
|
||
let(:user) { create_account }
|
||
|
||
let(:project) { project_with_default_policy(user) }
|
||
|
||
describe "unauthenticated" do
|
||
it "cannot perform authenticated operations" do
|
||
[
|
||
[:get, "/project"],
|
||
[:post, "/project", {name: "p-1"}],
|
||
[:delete, "/project/#{project.ubid}"]
|
||
].each do |method, path, body|
|
||
send(method, path, body)
|
||
|
||
expect(last_response).to have_api_error(401, "must include personal access token in Authorization header")
|
||
end
|
||
end
|
||
|
||
it "does not recognize invalid personal access tokens" do
|
||
account = Account[email: user.email]
|
||
pat = ApiKey.create_with_id(owner_table: "accounts", owner_id: account.id, used_for: "api", project_id: project.id)
|
||
|
||
header "Authorization", "Bearer pat-#{pat.ubid[0...-1]}-#{pat.key}"
|
||
get "/project"
|
||
expect(last_response.status).to eq(401)
|
||
|
||
header "Authorization", "Bearer pat-#{pat.ubid}-#{pat.key[0...-1]}"
|
||
get "/project"
|
||
expect(last_response.status).to eq(401)
|
||
|
||
header "Authorization", "Bearer pat-#{account.ubid}-#{pat.key}"
|
||
get "/project"
|
||
expect(last_response.status).to eq(401)
|
||
|
||
pat.update(is_valid: false)
|
||
header "Authorization", "Bearer pat-#{pat.ubid}-#{pat.key}"
|
||
get "/project"
|
||
expect(last_response.status).to eq(401)
|
||
end
|
||
end
|
||
|
||
describe "authenticated" do
|
||
before do
|
||
login_api
|
||
end
|
||
|
||
describe "list" do
|
||
it "success" do
|
||
project
|
||
get "/project"
|
||
|
||
expect(last_response.status).to eq(200)
|
||
parsed_body = JSON.parse(last_response.body)
|
||
expect(parsed_body["count"]).to eq(2)
|
||
end
|
||
|
||
it "invalid order column" do
|
||
project
|
||
get "/project?order_column=name"
|
||
|
||
expect(last_response).to have_api_error(400, "Validation failed for following fields: order_column")
|
||
end
|
||
|
||
it "invalid id" do
|
||
project
|
||
get "/project?start_after=invalid_id"
|
||
|
||
expect(last_response).to have_api_error(400, "Validation failed for following fields: start_after")
|
||
end
|
||
end
|
||
|
||
describe "create" do
|
||
it "success" do
|
||
project
|
||
post "/project", {
|
||
name: "test-project"
|
||
}.to_json
|
||
|
||
expect(last_response.status).to eq(200)
|
||
expect(JSON.parse(last_response.body)["name"]).to eq("test-project")
|
||
end
|
||
|
||
it "creates up to 10 projects per account" do
|
||
project
|
||
(10 - user.projects_dataset.count).times do |i|
|
||
post "/project", {
|
||
name: "test-project-#{i}"
|
||
}.to_json
|
||
|
||
expect(last_response.status).to eq(200)
|
||
expect(JSON.parse(last_response.body)["name"]).to eq("test-project-#{i}")
|
||
end
|
||
|
||
expect(user.projects_dataset.count).to eq(10)
|
||
|
||
post "/project", {
|
||
name: "test-project"
|
||
}.to_json
|
||
|
||
expect(last_response).to have_api_error(400, "Project limit exceeded. You can create up to 10 projects. Contact support@ubicloud.com if you need more.")
|
||
expect(user.projects_dataset.count).to eq(10)
|
||
end
|
||
end
|
||
|
||
describe "delete" do
|
||
it "success" do
|
||
delete "/project/#{project.ubid}"
|
||
|
||
expect(last_response.status).to eq(204)
|
||
|
||
expect(Project[project.id].visible).to be_falsey
|
||
expect(DB[:access_tag].where(project_id: project.id).count).to eq(0)
|
||
expect(SubjectTag.where(project_id: project.id).count).to eq(0)
|
||
expect(AccessControlEntry.where(project_id: project.id).count).to eq(0)
|
||
end
|
||
|
||
it "success with non-existing project" do
|
||
project_with_default_policy(user)
|
||
delete "/project/pj000000000000000000000000"
|
||
|
||
expect(last_response.status).to eq(204)
|
||
end
|
||
|
||
it "can not delete project when it has resources" do
|
||
Prog::Vm::Nexus.assemble("k y", project.id, name: "vm1")
|
||
|
||
delete "/project/#{project.ubid}"
|
||
|
||
expect(last_response).to have_api_error(409, "'#{project.name}' project has some resources. Delete all related resources first.")
|
||
end
|
||
|
||
it "not authorized" do
|
||
project_with_default_policy(user)
|
||
p = create_account("test@test.com").create_project_with_default_policy("project-1")
|
||
delete "/project/#{p.ubid}"
|
||
|
||
expect(last_response.status).to eq(204)
|
||
end
|
||
end
|
||
|
||
describe "show" do
|
||
it "success" do
|
||
get "/project/#{project.ubid}"
|
||
|
||
expect(last_response.status).to eq(200)
|
||
expect(JSON.parse(last_response.body)["name"]).to eq(project.name)
|
||
end
|
||
|
||
it "failure with unauthorized personal access token" do
|
||
project
|
||
AccessControlEntry.dataset.destroy
|
||
AccessControlEntry.create_with_id(project_id: project.id, subject_id: @pat.id, action_id: ActionType::NAME_MAP["Project:edit"])
|
||
|
||
get "/project/#{project.ubid}"
|
||
expect(last_response).to have_api_error(403, "Sorry, you don't have permission to continue with this request.")
|
||
end
|
||
|
||
it "not found" do
|
||
project
|
||
get "/project/pj000000000000000000000000"
|
||
|
||
expect(last_response).to have_api_error(404, "Sorry, we couldn’t find the resource you’re looking for.")
|
||
end
|
||
|
||
it "not authorized" do
|
||
project
|
||
u = create_account("test@test.com")
|
||
p = u.create_project_with_default_policy("project-1")
|
||
get "/project/#{p.ubid}"
|
||
|
||
expect(last_response).to have_api_error(404, "Sorry, we couldn’t find the resource you’re looking for.")
|
||
end
|
||
end
|
||
end
|
||
end
|