Files
ubicloud/spec/lib/authorization_spec.rb
Jeremy Evans 8a3e558ce4 Replace Postgres:Firewall:{view,edit} permissions with Postgres:{view,edit}
Postgres firewalls are not a separate object type.  There is
PostgresFirewallRule, but the Postgres:Firewall permissions
are tested against the PostgresResource, so for consistency,
it makes sense to keep the type:action format used by other
permissions, and not support colon as part of the type. After
further discussion, there is agreement that separate permissions
for Postgres firewall rules are not needed, so using the normal
Postgres view/edit permissions are fine.

While here, avoid N+1 fwedit access check queries in the
view/postgres/show template, where N is the number of firewall
rules.
2024-12-18 09:39:56 -08:00

229 lines
11 KiB
Ruby

# frozen_string_literal: true
require "sequel/model"
RSpec.describe Authorization do
let(:users) {
[
Account.create_with_id(email: "auth1@example.com"),
Account.create_with_id(email: "auth2@example.com")
]
}
let(:projects) { (0..1).map { users[_1].create_project_with_default_policy("project-#{_1}") } }
let(:vms) {
(0..3).map do |index|
ps = Prog::Vnet::SubnetNexus.assemble(projects[index / 2].id, name: "vm#{index}-ps", location: "hetzner-fsn1").subject
Prog::Vm::Nexus.assemble("key", projects[index / 2].id, name: "vm#{index}", private_subnet_id: ps.id)
end.map(&:subject)
}
let(:pg) {
Prog::Postgres::PostgresResourceNexus.assemble(
project_id: projects[0].id, location: "hetzner-fsn1", name: "pg0", target_vm_size: "standard-2", target_storage_size_gib: 128
).subject
}
let(:access_policy) { projects[0].access_policies.first }
after do
users.each(&:destroy)
end
before do
allow(Config).to receive(:postgres_service_project_id).and_return(projects[0].id)
end
describe "#matched_policies" do
it "without specific object" do
[
[[], SecureRandom.uuid, "Vm:view", 0],
[[], SecureRandom.uuid, ["Vm:view"], 0],
[[], SecureRandom.uuid, ["Vm:view"], 0],
[[], users[0].id, "Vm:view", 0],
[[], users[0].id, ["Vm:view"], 0],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:view", objects: projects[0].hyper_tag_name}], users[0].id, "Vm:view", 12],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:view", objects: projects[0].hyper_tag_name}], users[0].id, ["Vm:view", "Vm:create"], 12],
[[{subjects: [users[0].hyper_tag_name], actions: ["Vm:view"], objects: [projects[0].hyper_tag_name]}], users[0].id, "Vm:view", 12],
[[{subjects: [users[0].hyper_tag_name, users[1].hyper_tag_name], actions: ["Vm:view", "Vm:delete"], objects: [projects[0].hyper_tag_name]}], users[0].id, ["Vm:view", "Vm:create"], 12],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:view", objects: vms[0].hyper_tag_name(access_policy.project)}], users[0].id, "Vm:view", 1],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:view", objects: vms.map { _1.hyper_tag_name(access_policy.project) }}], users[0].id, "Vm:view", 2],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:delete", objects: vms[0].hyper_tag_name(access_policy.project)}], users[0].id, "Vm:view", 0],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:*", objects: vms[0].hyper_tag_name(access_policy.project)}], users[0].id, "Vm:view", 1],
[[{subjects: users[0].hyper_tag_name, actions: "*", objects: vms[0].hyper_tag_name(access_policy.project)}], users[0].id, "Vm:view", 1],
[[{subjects: users[0].hyper_tag_name, actions: "Postgres:view", objects: pg.hyper_tag_name(access_policy.project)}], users[0].id, "Postgres:edit", 0],
[[{subjects: users[0].hyper_tag_name, actions: "Postgres:edit", objects: pg.hyper_tag_name(access_policy.project)}], users[0].id, "Postgres:view", 0]
].each do |policies, subject_id, actions, matched_count|
access_policy.update(body: {acls: policies})
expect(described_class.matched_policies(subject_id, actions).count).to eq(matched_count)
end
end
it "with specific object" do
[
[[], SecureRandom.uuid, "Vm:view", SecureRandom.uuid, 0],
[[], SecureRandom.uuid, ["Vm:view"], SecureRandom.uuid, 0],
[[], SecureRandom.uuid, ["Vm:view"], vms[0].id, 0],
[[], users[0].id, ["Vm:view"], vms[0].id, 0],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:view", objects: projects[0].hyper_tag_name}], users[0].id, "Vm:view", vms[0].id, 1],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:view", objects: projects[0].hyper_tag_name}], users[0].id, ["Vm:view", "Vm:create"], vms[0].id, 1],
[[{subjects: [users[0].hyper_tag_name], actions: ["Vm:view"], objects: [projects[0].hyper_tag_name]}], users[0].id, "Vm:view", vms[0].id, 1],
[[{subjects: [users[0].hyper_tag_name, users[1].hyper_tag_name], actions: ["Vm:view", "Vm:delete"], objects: [projects[0].hyper_tag_name]}], users[0].id, ["Vm:view", "Vm:create"], vms[0].id, 1],
[[{subjects: users[0].hyper_tag_name, actions: "Vm:delete", objects: projects[0].hyper_tag_name}], users[0].id, "Vm:view", vms[0].id, 0],
[[{subjects: [users[0].hyper_tag_name], actions: ["Vm:view"], objects: [projects[0].hyper_tag_name, projects[0].hyper_tag_name]}], users[0].id, "Vm:view", vms[0].id, 1]
].each do |policies, subject_id, actions, object_id, matched_count|
access_policy.update(body: {acls: policies})
expect(described_class.matched_policies(subject_id, actions, object_id).count).to eq(matched_count)
end
end
end
describe "#has_permission?" do
it "returns true when has matched policies" do
expect(described_class.has_permission?(users[0].id, "Vm:view", vms[0].id)).to be(true)
end
it "returns false when has no matched policies" do
access_policy.update(body: [])
expect(described_class.has_permission?(users[0].id, "Vm:view", vms[0].id)).to be(false)
end
end
describe "#authorize" do
it "not raises error when has matched policies" do
expect { described_class.authorize(users[0].id, "Vm:view", vms[0].id) }.not_to raise_error
end
it "raises error when has no matched policies" do
access_policy.update(body: [])
expect { described_class.authorize(users[0].id, "Vm:view", vms[0].id) }.to raise_error Authorization::Unauthorized
end
end
describe "#authorized_resources_dataset" do
it "returns resource ids when has matched policies" do
ids = [vms[0].id, vms[1].id, projects[0].id, users[0].id, vms[0].private_subnets[0].id, vms[1].private_subnets[0].id, vms[0].firewalls[0].id, vms[1].firewalls[0].id]
expect(described_class.authorized_resources_dataset(users[0].id, "Vm:view").map(:tagged_id).sort).to eq(ids.sort)
end
it "returns no resource ids when has no matched policies" do
access_policy.update(body: [])
expect(described_class.authorized_resources_dataset(users[0].id, "Vm:view")).to be_empty
end
end
describe "#expand_actions" do
it "returns expanded actions" do
[
["*", ["*"]],
["Vm:*", ["Vm:*", "*"]],
["Vm:view", ["Vm:view", "Vm:*", "*"]],
[["Vm:view", "PrivateSubnet:view"], ["Vm:view", "PrivateSubnet:view", "Vm:*", "PrivateSubnet:*", "*"]]
].each do |actions, expected|
expect(described_class.expand_actions(actions)).to match_array(expected)
end
end
end
describe "#ManagedPolicy" do
it "apply" do
expect(AccessPolicy[project_id: projects[0].id, name: "member", managed: true]).to be_nil
described_class::ManagedPolicy::Member.apply(projects[0], [users[0], nil, users[1]])
acl = AccessPolicy[project_id: projects[0].id, name: "member", managed: true].body["acls"].first
expect(acl["subjects"]).to contain_exactly(users[0].hyper_tag_name)
expect(acl["actions"]).to eq(["Vm:*", "PrivateSubnet:*", "Firewall:*", "Postgres:*", "Project:view", "Project:github", "InferenceEndpoint:view"])
expect(acl["objects"]).to eq(["project/#{projects[0].ubid}"])
users[1].associate_with_project(projects[0])
described_class::ManagedPolicy::Member.apply(projects[0], [users[1]], append: true)
expect(AccessPolicy[project_id: projects[0].id, name: "member", managed: true].body["acls"].first["subjects"]).to contain_exactly(users[0].hyper_tag_name, users[1].hyper_tag_name)
described_class::ManagedPolicy::Member.apply(projects[0], [])
expect(AccessPolicy[project_id: projects[0].id, name: "member", managed: true].body["acls"].first["subjects"]).to eq([])
end
it "from_name" do
expect(described_class::ManagedPolicy.from_name("admin")).to eq(described_class::ManagedPolicy::Admin)
expect(described_class::ManagedPolicy.from_name("invalid")).to be_nil
end
end
describe "#Dataset" do
it "returns authorized resources" do
ids = [vms[0].id, vms[1].id]
expect(Vm.authorized(users[0].id, "Vm:view").select_map(:id).sort).to eq(ids.sort)
end
it "returns no authorized resources" do
expect(Vm.authorized(users[0].id, "Vm:view").select_map(:id).sort).to eq([])
end
end
describe "#HyperTagMethods" do
it "hyper_tag_name" do
expect(users[0].hyper_tag_name).to eq("user/auth1@example.com")
p = vms[0].projects.first
expect(vms[0].hyper_tag_name(p)).to eq("project/#{p.ubid}/location/eu-central-h1/vm/vm0")
expect(projects[0].hyper_tag_name).to eq("project/#{projects[0].ubid}")
end
it "hyper_tag_name error" do
c = Class.new(Sequel::Model) do
include Authorization::HyperTagMethods
end
expect { c.new.hyper_tag_name }.to raise_error NoMethodError
end
it "hyper_tag methods" do
project = Project.create_with_id(name: "test")
expect(project.hyper_tag(project)).to be_nil
tag = project.associate_with_project(project)
expect(project.hyper_tag(project)).to exist
vms.each { _1.tag(tag) }
expect(AppliedTag.where(access_tag_id: tag.id).count).to eq(5)
project.dissociate_with_project(project)
expect(project.hyper_tag(project)).to be_nil
expect(AppliedTag.where(access_tag_id: tag.id).count).to eq(0)
end
it "associate/dissociate with project" do
project = Project.create_with_id(name: "test")
project.associate_with_project(project)
users[0].associate_with_project(project)
expect(project.applied_access_tags.count).to eq(1)
expect(users[0].applied_access_tags.count).to eq(4)
users[0].dissociate_with_project(project)
project.dissociate_with_project(project)
expect(project.reload.applied_access_tags.count).to eq(0)
expect(users[0].reload.applied_access_tags.count).to eq(2)
end
it "does not associate/dissociate with nil project" do
project = Project.create_with_id(name: "test")
expect(project.associate_with_project(nil)).to be_nil
expect(project.applied_access_tags.count).to eq(0)
expect(project.dissociate_with_project(nil)).to be_nil
expect(project.applied_access_tags.count).to eq(0)
end
end
describe "#TaggableMethods" do
it "can tag" do
tag = projects[1].hyper_tag(projects[1])
expect(vms[0].applied_access_tags.include?(tag)).to be(false)
vms[0].tag(tag)
expect(vms[0].reload.applied_access_tags.include?(tag)).to be(true)
end
it "can untag" do
tag = projects[0].hyper_tag(projects[0])
expect(vms[0].applied_access_tags.include?(tag)).to be(true)
vms[0].untag(tag)
expect(vms[0].reload.applied_access_tags.include?(tag)).to be(false)
end
end
end