Files
ubicloud/spec/routes/api/project_spec.rb
Jeremy Evans 30ff9b38a4 Authorize requests using personal access token if provided
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.
2024-12-04 10:18:55 -08:00

179 lines
5.7 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 couldnt find the resource youre 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