Files
ubicloud/spec/lib/authorization_spec.rb
Furkan Sahin 12dbeb57a1 Update location references with foreign key in the controlplane
We are basically updating the location references everywhere with a
location id and adding the location relationship to the models to be
able to fetch location names when needed.
This also makes the LocationNameConverter model obsolete, so we are
removing it.

Use model id as value for Sequel::Model in resource creation form

Use id of the location as preselected value in Postgres update form
2025-03-23 15:48:19 +01:00

263 lines
15 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_id: Location::HETZNER_FSN1_ID).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_id: Location::HETZNER_FSN1_ID, name: "pg0", target_vm_size: "standard-2", target_storage_size_gib: 128
).subject
}
after do
users.each(&:destroy)
end
before do
allow(Config).to receive(:postgres_service_project_id).and_return(projects[0].id)
end
def add_separate_aces(policies, project_id: projects[0].id)
ace_subjects, ace_actions, ace_objects = policies.values_at(:subjects, :actions, :objects)
Array(ace_subjects).each do |subject_id|
Array(ace_actions).each do |action|
action_id = ActionType::NAME_MAP.fetch(action) { ActionTag[project_id: nil, name: action].id } if action
Array(ace_objects).each do |object_id|
AccessControlEntry.create_with_id(project_id:, subject_id:, action_id:, object_id:)
end
end
end
end
def add_single_ace(policies, project_id: projects[0].id)
ace_subjects, ace_actions, ace_objects = policies.values_at(:subjects, :actions, :objects)
subject_tag = SubjectTag.create_with_id(project_id:, name: "S")
Array(ace_subjects).each do |subject_id|
subject_tag.add_subject(subject_id)
end
subject_tag = yield subject_tag if block_given?
action_id = unless ace_actions == [nil]
action_tag = ActionTag.create_with_id(project_id:, name: "A")
Array(ace_actions).each_with_index do |action_id, i|
action_id = ActionType::NAME_MAP.fetch(action_id) { ActionTag[project_id: nil, name: action_id].id }
action_tag.add_action(action_id)
end
action_tag = yield action_tag if block_given?
action_tag.id
end
object_id = unless ace_objects == [nil]
object_tag = ObjectTag.create_with_id(project_id:, name: "A")
Array(ace_objects).each do |object_id|
object_tag.add_object(object_id)
end
object_tag = yield object_tag if block_given?
object_tag.id
end
AccessControlEntry.create_with_id(project_id:, subject_id: subject_tag.id, action_id:, object_id:)
end
def add_single_ace_with_nested_tags(policies, project_id: projects[0].id)
add_single_ace(policies, project_id:) do |tag|
3.times do |i|
old_tag = tag
tag = tag.class.create_with_id(project_id: tag.project_id, name: i.to_s)
tag.send(:"add_#{tag.class.name.delete_suffix("Tag").downcase}", old_tag.id)
end
tag
end
end
# rubocop:disable RSpec/MissingExpectationTargetMethod
describe "#matched_policies" do
it "without specific object" do
AccessControlEntry.dataset.destroy
project_id = projects[0].id
[
[{}, 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].id, actions: "Vm:all", objects: [nil]}, users[0].id, "Vm:view", 1],
[{subjects: users[0].id, actions: "Vm:all", objects: [nil]}, users[0].id, "Postgres:view", 0],
[{subjects: users[0].id, actions: "Member", objects: [nil]}, users[0].id, "Vm:view", 1],
[{subjects: users[0].id, actions: "Member", objects: [nil]}, users[0].id, "Project:edit", 0],
[{subjects: users[0].id, actions: "Vm:view", objects: [nil]}, users[0].id, "Vm:view", 1],
[{subjects: users[0].id, actions: "Vm:view", objects: [nil]}, users[0].id, ["Vm:view", "Vm:create"], 1],
[{subjects: users[0].id, actions: ["Vm:view", "Vm:delete"], objects: [nil]}, users[0].id, ["Vm:view", "Vm:create"], 1],
[{subjects: users[0].id, actions: "Vm:view", objects: vms[0].id}, users[0].id, "Vm:view", 1],
[{subjects: users[0].id, actions: "Vm:view", objects: [vms[0].id, vms[1].id]}, users[0].id, "Vm:view", 2],
[{subjects: users[0].id, actions: "Vm:delete", objects: vms[0].id}, users[0].id, "Vm:view", 0],
[{subjects: users[0].id, actions: %w[Vm:view Vm:delete], objects: vms[0].id}, users[0].id, "Vm:view", 1],
[{subjects: users[0].id, actions: [nil], objects: vms[0].id}, users[0].id, "Vm:view", 1],
[{subjects: users[0].id, actions: "Postgres:view", objects: pg.id}, users[0].id, "Postgres:edit", 0],
[{subjects: users[0].id, actions: "Postgres:edit", objects: pg.id}, users[0].id, "Postgres:view", 0],
[{subjects: users[0].id, actions: "Postgres:view", objects: pg.id}, users[0].id, "Postgres:view", 1]
].each do |policies, subject_id, actions, matched_count|
DB.transaction(rollback: :always) do
add_separate_aces(policies)
expect(described_class.matched_policies(project_id, subject_id, actions).count).to eq(matched_count)
expect(described_class.all_permissions(project_id, subject_id, nil) & Array(actions)).send((matched_count == 0) ? :to : :not_to, be_empty)
end
DB.transaction(rollback: :always) do
add_single_ace(policies)
expect(described_class.matched_policies(project_id, subject_id, actions).count).to eq((matched_count == 0) ? 0 : 1)
expect(described_class.all_permissions(project_id, subject_id, nil) & Array(actions)).send((matched_count == 0) ? :to : :not_to, be_empty)
end
DB.transaction(rollback: :always) do
add_single_ace_with_nested_tags(policies)
expect(described_class.matched_policies(project_id, subject_id, actions).count).to eq((matched_count == 0) ? 0 : 1)
expect(described_class.all_permissions(project_id, subject_id, nil) & Array(actions)).send((matched_count == 0) ? :to : :not_to, be_empty)
end
end
end
it "with specific object" do
AccessControlEntry.dataset.destroy
project_id = projects[0].id
# Backwards compatibility for old TYPE_ETC ubid (etkjnpyp1dst3n9d2mct7s71rh in this example)
api_key_id = ApiKey.create_with_id(owner_table: "project", owner_id: project_id, used_for: "inference_endpoint", project_id:) { |api_key| api_key.id = "9cab6f58-2dce-85da-aa5a-2a3347c9c388" }.id
[
[{subjects: [users[0].id], actions: ["Vm:view"], objects: api_key_id}, users[0].id, "Vm:view", api_key_id, 1],
[{}, SecureRandom.uuid, "Vm:view", UBID.from_uuidish(SecureRandom.uuid).to_s.sub(/\A../, "00"), 0],
[{}, SecureRandom.uuid, ["Vm:view"], UBID.from_uuidish(SecureRandom.uuid).to_s.sub(/\A../, "00"), 0],
[{}, SecureRandom.uuid, ["Vm:view"], vms[0].id, 0],
[{}, users[0].id, ["Vm:view"], vms[0].id, 0],
[{subjects: users[0].id, actions: "Vm:all", objects: [nil]}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: "Vm:all", objects: [nil]}, users[0].id, "Postgres:view", vms[0].id, 0],
[{subjects: users[0].id, actions: "Member", objects: [nil]}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: "Member", objects: [nil]}, users[0].id, "Project:edit", vms[0].id, 0],
[{subjects: users[0].id, actions: "Vm:view", objects: [nil]}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: "Vm:view", objects: [nil]}, users[0].id, ["Vm:view", "Vm:create"], vms[0].id, 1],
[{subjects: [users[0].id], actions: ["Vm:view"], objects: [nil]}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: ["Vm:view", "Vm:delete"], objects: [nil]}, users[0].id, ["Vm:view", "Vm:create"], vms[0].id, 1],
[{subjects: users[0].id, actions: "Vm:delete", objects: [nil]}, users[0].id, "Vm:view", vms[0].id, 0],
[{subjects: [users[0].id], actions: ["Vm:view"], objects: [nil]}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: "Vm:view", objects: vms[0].id}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: "Vm:view", objects: [vms[0].id, vms[1].id]}, users[0].id, ["Vm:view", "Vm:create"], vms[0].id, 1],
[{subjects: [users[0].id], actions: ["Vm:view"], objects: vms[0].id}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: ["Vm:view", "Vm:delete"], objects: vms[0].id}, users[0].id, ["Vm:view", "Vm:create"], vms[0].id, 1],
[{subjects: users[0].id, actions: "Vm:delete", objects: vms[0].id}, users[0].id, "Vm:view", vms[0].id, 0],
[{subjects: [users[0].id], actions: ["Vm:view"], objects: vms[0].id}, users[0].id, "Vm:view", vms[0].id, 1],
[{subjects: users[0].id, actions: "Vm:view", objects: vms[1].id}, users[0].id, "Vm:view", vms[0].id, 0],
[{subjects: [users[0].id], actions: ["Vm:view"], objects: vms[1].id}, users[0].id, "Vm:view", vms[0].id, 0],
[{subjects: users[0].id, actions: ["Vm:view", "Vm:delete"], objects: vms[1].id}, users[0].id, ["Vm:view", "Vm:create"], vms[0].id, 0],
[{subjects: users[0].id, actions: "Vm:delete", objects: vms[1].id}, users[0].id, "Vm:view", vms[0].id, 0],
[{subjects: [users[0].id], actions: ["Vm:view"], objects: vms[1].id}, users[0].id, "Vm:view", vms[0].id, 0]
].each do |policies, subject_id, actions, object_id, matched_count|
DB.transaction(rollback: :always) do
add_separate_aces(policies)
expect(described_class.matched_policies(project_id, subject_id, actions, object_id).count).to eq(matched_count)
expect(described_class.all_permissions(project_id, subject_id, object_id) & Array(actions)).send((matched_count == 0) ? :to : :not_to, be_empty)
end
DB.transaction(rollback: :always) do
add_single_ace(policies)
expect(described_class.matched_policies(project_id, subject_id, actions, object_id).count).to eq((matched_count == 0) ? 0 : 1)
expect(described_class.all_permissions(project_id, subject_id, object_id) & Array(actions)).send((matched_count == 0) ? :to : :not_to, be_empty)
end
DB.transaction(rollback: :always) do
add_single_ace_with_nested_tags(policies)
expect(described_class.matched_policies(project_id, subject_id, actions, object_id).count).to eq((matched_count == 0) ? 0 : 1)
expect(described_class.all_permissions(project_id, subject_id, object_id) & Array(actions)).send((matched_count == 0) ? :to : :not_to, be_empty)
end
end
end
end
# rubocop:enable RSpec/MissingExpectationTargetMethod
describe "#has_permission?" do
it "returns true when has matched policies" do
expect(described_class.has_permission?(projects[0].id, users[0].id, "Vm:view", vms[0].id)).to be(true)
end
it "returns false when has no matched policies" do
AccessControlEntry.dataset.destroy
expect(described_class.has_permission?(projects[0].id, users[0].id, "Vm:view", vms[0].id)).to be(false)
end
end
describe "#authorize" do
it "does not raise error when there existed a matching access control entry when using UUID object_id" do
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", vms[0].id) }.not_to raise_error
end
it "does not raise error when there existed a matching access control entry when using UBID object_id" do
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", vms[0].ubid) }.not_to raise_error
end
it "does not raise error when there existed a matching access control entry when object_id in in project" do
st = SubjectTag.create_with_id(project_id: projects[0].id, name: "test")
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", projects[0].id) }.not_to raise_error
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", vms[0].id) }.not_to raise_error
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", st.id) }.not_to raise_error
end
it "raises error when has matched policies when object is in project" do
st = SubjectTag.create_with_id(project_id: projects[1].id, name: "test")
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", projects[1].id) }.to raise_error Authorization::Unauthorized
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", vms[3].id) }.to raise_error Authorization::Unauthorized
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", st.id) }.to raise_error Authorization::Unauthorized
end
it "raises error when non-UBID/non-UUID is used" do
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", "some-garbage") }.to raise_error UBIDParseError
end
it "raises error when has no matched policies" do
AccessControlEntry.dataset.destroy
expect { described_class.authorize(projects[0].id, users[0].id, "Vm:view", vms[0].id) }.to raise_error Authorization::Unauthorized
end
end
describe ".dataset_authorize" do
it "returns authorized resources for user and project and action when user has full permissions" do
vms
expect(described_class.dataset_authorize(Vm.dataset, projects[0].id, users[0].id, "Vm:view").select_map(:id).sort).to eq([vms[0].id, vms[1].id].sort)
expect(described_class.dataset_authorize(Vm.dataset, projects[0].id, users[1].id, "Vm:view").select_map(:id)).to be_empty
expect(described_class.dataset_authorize(Vm.dataset, projects[1].id, users[0].id, "Vm:view").select_map(:id)).to be_empty
expect(described_class.dataset_authorize(Vm.dataset, projects[1].id, users[1].id, "Vm:view").select_map(:id).sort).to eq([vms[2].id, vms[3].id].sort)
end
{
add_separate_aces: "direct permissions",
add_single_ace: "indirect permissions via tag",
add_single_ace_with_nested_tags: "indirect permissions via nested tag"
}.each do |method, desc|
it "returns authorized resources for user and project and action when user has #{desc}" do
vms
AccessControlEntry.dataset.destroy
send(method, {subjects: users[0].id, actions: "Vm:view", objects: vms[0].id})
send(method, {subjects: users[1].id, actions: "Vm:view", objects: vms[3].id}, project_id: projects[1].id)
expect(described_class.dataset_authorize(Vm.dataset, projects[0].id, users[0].id, "Vm:view").select_map(:id)).to eq([vms[0].id])
expect(described_class.dataset_authorize(Vm.dataset, projects[0].id, users[1].id, "Vm:view").select_map(:id)).to be_empty
expect(described_class.dataset_authorize(Vm.dataset, projects[1].id, users[0].id, "Vm:view").select_map(:id)).to be_empty
expect(described_class.dataset_authorize(Vm.dataset, projects[1].id, users[1].id, "Vm:view").select_map(:id)).to eq([vms[3].id])
end
end
end
end