If a personal access token is provided, after performing authorization for the account, perform an additional authorization specific to the personal access token. Once the UI is added, this will allow a user to create multiple personal access tokens, each with a specific access policy, which allows restricting what each personal access token can do separately. There are two approaches that could have been used for this: * Multiple authorizations (this approach) * Single authorization The issue with the single authorization approach is that it is more complex, because we would need to ensure at all times that personal access token authority does not exceed account authority. I think such an approach is risky unless the account access policy is immutable, and even though, it's more complex as you have to write a verifier to compare personal access token permissions with account permissions to ensure authority is not exceeded. With the multiple authorization approach, most of that complexity goes away, though it does require two authorization queries instead of one. It doesn't matter if the personal access token permissions exceed the account permissions, because the account permissions will be checked first, and if the request is not authorized for the account, it will be rejected before even checking the personal access token permissions. One issue I found is that you cannot delete a project using a personal access token, because the personal access token's access/applied tags are considered dependencies, and you cannot delete a project with dependencies. Maybe we want the code to automatically handle this case? To keep the api specs passing, this grants admin permissions to the personal access token used by default. However, it adds a couple of tests using non-admin personal access tokens, one to test the success case and one to test the failure case.
179 lines
5.7 KiB
Ruby
179 lines
5.7 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, "Please login to continue")
|
||
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
|
||
|
||
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
|
||
|
||
{
|
||
"with login api" => false,
|
||
"with personal access token" => true
|
||
}.each do |desc, use_pat|
|
||
describe "authenticated #{desc}" do
|
||
before do
|
||
login_api(user.email, use_pat:)
|
||
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
|
||
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
|
||
end
|
||
|
||
describe "delete" do
|
||
# You cannot currently delete a project using a personal access token,
|
||
# because the access/applied tags are considered dependencies, so an
|
||
# authorized request will return 409, and an unauthorized request will
|
||
# return 403.
|
||
unless use_pat
|
||
it "success" do
|
||
delete "/project/#{project.ubid}"
|
||
|
||
expect(last_response.status).to eq(204)
|
||
|
||
expect(Project[project.id].visible).to be_falsey
|
||
expect(AccessTag.where(project_id: project.id).count).to eq(0)
|
||
expect(AccessPolicy.where(project_id: project.id).count).to eq(0)
|
||
end
|
||
end
|
||
|
||
it "success with non-existing project" do
|
||
delete "/project/non_existing_id"
|
||
|
||
expect(last_response.status).to eq(204)
|
||
end
|
||
|
||
it "can not delete project when it has resources" do
|
||
Prog::Vm::Nexus.assemble("key", 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
|
||
u = create_account("test@test.com")
|
||
p = u.create_project_with_default_policy("project-1")
|
||
delete "/project/#{p.ubid}"
|
||
|
||
expect(last_response).to have_api_error(403, "Sorry, you don't have permission to continue with this request.")
|
||
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
|
||
|
||
if use_pat
|
||
it "success with authorized personal access token" do
|
||
project = user.create_project_with_default_policy("project-1")
|
||
@pat.associate_with_project(project)
|
||
Authorization::ManagedPolicy::ManagedPolicyClass.new("pat-1", ["Project:view"]).apply(project, [@pat])
|
||
|
||
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 = user.create_project_with_default_policy("project-1")
|
||
@pat.associate_with_project(project)
|
||
Authorization::ManagedPolicy::ManagedPolicyClass.new("pat-1", ["Project:edit"]).apply(project, [@pat])
|
||
|
||
get "/project/#{project.ubid}"
|
||
expect(last_response).to have_api_error(403, "Sorry, you don't have permission to continue with this request.")
|
||
end
|
||
end
|
||
|
||
it "not found" do
|
||
get "/project/08s56d4kaj94xsmrnf5v5m3mav"
|
||
|
||
expect(last_response).to have_api_error(404, "Sorry, we couldn’t find the resource you’re looking for.")
|
||
end
|
||
|
||
it "not authorized" do
|
||
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(403, "Sorry, you don't have permission to continue with this request.")
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|