Files
ubicloud/spec/ubid_spec.rb
Jeremy Evans 488f2750ac Remove AccessPolicy
AccessPolicy was still referenced in a bunch of specs, where it had
no effect because the specs weren't testing what they thought they
were testing.  Replace AccessPolicy creation with equivalent
AccessControlEntry creation, and check that we can get to the
page we were expected to get to.  We should really go further and
check that if you remove an ACE allowing edit/destroy after
displaying the page (which shows the button), the button doesn't
work due to the access control violation.  However, that can be
added later.

Add association dependencies for access_control_entries and
subject_tags to Project.  I'm not sure these are ever used, since
we soft-delete projects instead of actually deleting them, so this
is just cargo culting because access_policies were listed.
2025-01-09 09:55:55 -08:00

367 lines
16 KiB
Ruby

# frozen_string_literal: true
RSpec.describe UBID do
let(:all_types) { described_class.constants.select { _1.start_with?("TYPE_") }.map { described_class.const_get(_1) } }
it ".generate_vanity_action_type supports creating vanity ubids for action types" do
expect(described_class.generate_vanity_action_type("Project:view").to_s).to eq "ttzzzzzzzz021gzzz0pj0v1ew0"
expect(described_class.generate_vanity_action_type("ObjectTag:add").to_s).to eq "ttzzzzzzzz021gzzzz0t00add0"
end
it ".generate_vanity_action_tag supports creating vanity ubids for action tags" do
expect(described_class.generate_vanity_action_tag("Vm:all").to_s).to eq "tazzzzzzzz021gzzzz0vm0a111"
expect(described_class.generate_vanity_action_tag("Member").to_s).to eq "tazzzzzzzz021gzzzz0member0"
end
it ".generate_vanity raises if prefix or suffix is too long" do
expect { described_class.generate_vanity("tt", "foo", "bar") }.to raise_error(RuntimeError)
expect { described_class.generate_vanity("tt", "fo", "barbazqu") }.to raise_error(RuntimeError)
end
it "can set_bits" do
expect(described_class.set_bits(0, 0, 7, 0xab)).to eq(0xab)
expect(described_class.set_bits(0xab, 8, 12, 0xc)).to eq(0xcab)
expect(described_class.set_bits(0xcab, 40, 48, 0x12)).to eq(0x120000000cab)
end
it "can get bits" do
expect(described_class.get_bits(0x120000000cab, 8, 11)).to eq(0xc)
end
it "can convert to base32" do
tests = [
["0", 0], ["o", 0], ["A", 10], ["e", 14], ["m", 20], ["S", 25], ["z", 31]
]
tests.each {
expect(described_class.to_base32(_1[0])).to eq(_1[1])
}
end
it "can extract bits as hex" do
expect(described_class.extract_bits_as_hex(0x12345, 1, 3)).to eq("234")
expect(described_class.extract_bits_as_hex(0xabcdef00012, 4, 5)).to eq("cdef0")
expect(described_class.extract_bits_as_hex(0xabcdef00012, 1, 4)).to eq("0001")
expect(described_class.extract_bits_as_hex(0xabcdef00012, 9, 5)).to eq("000ab")
end
it "can encode multiple numbers" do
expect(described_class.from_base32_n(10 + 20 * 32 + 1 * 32 * 32, 3)).to eq("1ma")
end
it "fails to convert U to base32" do
expect { described_class.to_base32("u") }.to raise_error RuntimeError, "Invalid base32 encoding: U"
end
it "can convert from base32" do
tests = [
[0, "0"], [12, "c"], [16, "g"], [20, "m"], [26, "t"], [28, "w"], [31, "z"]
]
tests.each {
expect(described_class.from_base32(_1[0])).to eq(_1[1])
}
end
it "can convert multiple from base32" do
expect(described_class.to_base32_n("1MA")).to eq(10 + 20 * 32 + 1 * 32 * 32)
end
it "fails to convert from out of range numbers" do
expect {
described_class.from_base32(-1)
}.to raise_error RuntimeError, "Invalid base32 number: -1"
expect {
described_class.from_base32(32)
}.to raise_error RuntimeError, "Invalid base32 number: 32"
end
it "can calculate parity" do
expect(described_class.parity(0b10101)).to eq(1)
expect(described_class.parity(0b101011)).to eq(0)
end
it "can create from parts & encode it & parse it" do
id = described_class.from_parts(1000, "ab", 3, 12292929)
expect(described_class.parse(id.to_s).to_i).to eq(id.to_i)
end
it "can generate random ids" do
ubid1 = described_class.generate(UBID::TYPE_VM)
ubid2 = described_class.parse(ubid1.to_s)
expect(ubid2.to_i).to eq(ubid1.to_i)
end
it "can convert to and from uuid" do
ubid1 = described_class.generate(UBID::TYPE_ACCESS_TAG)
ubid2 = described_class.from_uuidish(ubid1.to_uuid)
expect(ubid2.to_s).to eq(ubid1.to_s)
end
it "can convert from and to uuid" do
uuid1 = "550e8400-e29b-41d4-a716-446655440000"
uuid2 = described_class.from_uuidish(uuid1).to_uuid
expect(uuid2).to eq(uuid1)
end
it "fails to parse if length is not 26" do
expect {
described_class.parse("123456")
}.to raise_error RuntimeError, "Invalid encoding length: 6"
end
it "fails to parse if top bits parity is incorrect" do
invalid_ubid = "vm164pbm96ba3grq6vbsvjb3ax"
expect {
described_class.parse(invalid_ubid)
}.to raise_error RuntimeError, "Invalid top bits parity"
end
it "fails to parse if bottom bits parity is incorrect" do
invalid_ubid = "vm064pbm96ba3grq6vbsvjb3az"
expect {
described_class.parse(invalid_ubid)
}.to raise_error RuntimeError, "Invalid bottom bits parity"
end
it "generates random timestamp for most types" do
(all_types - described_class::CURRENT_TIMESTAMP_TYPES).each { |type|
# generate 10 ids with this type, and verify that their timestamp
# part is not close
ubids = (1..10).map { described_class.generate(type).to_i }
max_ubid = ubids.max
min_ubid = ubids.min
# Timestamp is the left 48 bits of the 128-bit ubid.
time_difference = (max_ubid >> 80) - (min_ubid >> 80)
# Timestamp parts are random, so with a very high probability
# the difference between them will be > 8ms.
expect(time_difference).to be > 8
}
end
it "generates clock timestamp for strand and semaphore" do
described_class::CURRENT_TIMESTAMP_TYPES.each { |type|
# generate 10 ids with this type, and verify that their timestamp
# part is close
ubids = (1..10).map { described_class.generate(type).to_i }
max_ubid = ubids.max
min_ubid = ubids.min
# Timestamp is the left 48 bits of the 128-bit ubid.
time_difference = (max_ubid >> 80) - (min_ubid >> 80)
# Timestamp parts are sequential, so with a very high probability
# the difference between them will be < 128ms.
expect(time_difference).to be < 128
}
end
it "uses canonical type characters" do
# In crockford's base32 multiple characters can map to a single number,
# for example ["1","I","L"] all map to 1. However, the reverse (number
# -> character) only uses one canonical one (e.g. 1 maps to "1").
# This test ensures we only use canonical characters in our type
# constants, so ids are actually prefixed by those constants.
#
# See https://www.crockford.com/base32.html
all_types.each { |type|
ubid = described_class.generate(type).to_s
expect(ubid).to start_with type
}
end
it "has unique type identifiers" do
expect(all_types.uniq.length).to eq(all_types.length)
end
it "generates ids with proper prefix" do
sshable = Sshable.create_with_id
expect(sshable.ubid).to start_with UBID::TYPE_SSHABLE
host = VmHost.create(location: "x") { _1.id = sshable.id }
si = SpdkInstallation.create(version: "v1", allocation_weight: 100, vm_host_id: host.id) { _1.id = host.id }
vm = create_vm
expect(vm.ubid).to start_with UBID::TYPE_VM
dev = StorageDevice.create(name: "x", available_storage_gib: 1, total_storage_gib: 1, vm_host_id: host.id) { _1.id = host.id }
sv = VmStorageVolume.create_with_id(vm_id: vm.id, size_gib: 5, disk_index: 0, boot: false, spdk_installation_id: si.id, storage_device_id: dev.id)
expect(sv.ubid).to start_with UBID::TYPE_VM_STORAGE_VOLUME
kek = StorageKeyEncryptionKey.create_with_id(algorithm: "x", key: "x", init_vector: "x", auth_data: "x")
expect(kek.ubid).to start_with UBID::TYPE_STORAGE_KEY_ENCRYPTION_KEY
account = Account.create_with_id(email: "x@y.net")
expect(account.ubid).to start_with UBID::TYPE_ACCOUNT
prj = account.create_project_with_default_policy("x")
expect(prj.ubid).to start_with UBID::TYPE_PROJECT
st = SubjectTag.create_with_id(project_id: prj.id, name: "T")
expect(st.ubid).to start_with UBID::TYPE_SUBJECT_TAG
at = ActionTag.create_with_id(project_id: prj.id, name: "T")
expect(at.ubid).to start_with UBID::TYPE_ACTION_TAG
ot = ObjectTag.create_with_id(project_id: prj.id, name: "T")
expect(ot.ubid).to start_with UBID::TYPE_OBJECT_TAG
ace = AccessControlEntry.create_with_id(project_id: prj.id, subject_id: st.id)
expect(ace.ubid).to start_with UBID::TYPE_ACCESS_CONTROL_ENTRY
expect(ActionType.first.ubid).to start_with UBID::TYPE_ACTION_TYPE
atag = AccessTag.create_with_id(project_id: prj.id, hyper_tag_table: "x", name: "x")
expect(atag.ubid).to start_with UBID::TYPE_ACCESS_TAG
subnet = PrivateSubnet.create_with_id(net6: "0::0", net4: "127.0.0.1", name: "x", location: "x")
expect(subnet.ubid).to start_with UBID::TYPE_PRIVATE_SUBNET
nic = Nic.create_with_id(
private_ipv6: "fd10:9b0b:6b4b:8fbb::/128",
private_ipv4: "10.0.0.12/32",
mac: "00:11:22:33:44:55",
encryption_key: "0x30613961313636632d653765372d343434372d616232392d376561343432623562623065",
private_subnet_id: subnet.id,
name: "def-nic"
)
expect(nic.ubid).to start_with UBID::TYPE_NIC
tun = IpsecTunnel.create_with_id(src_nic_id: nic.id, dst_nic_id: nic.id)
expect(tun.ubid).to start_with UBID::TYPE_IPSEC_TUNNEL
adr = Address.create_with_id(cidr: "192.168.1.0/24", routed_to_host_id: host.id)
expect(adr.ubid).to start_with UBID::TYPE_ADDRESS
vm_adr = AssignedVmAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, dst_vm_id: vm.id)
expect(vm_adr.ubid).to start_with UBID::TYPE_ASSIGNED_VM_ADDRESS
host_adr = AssignedHostAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, host_id: host.id)
expect(host_adr.ubid).to start_with UBID::TYPE_ASSIGNED_HOST_ADDRESS
strand = Strand.create_with_id(prog: "x", label: "y")
expect(strand.ubid).to start_with UBID::TYPE_STRAND
semaphore = Semaphore.create_with_id(strand_id: strand.id, name: "z")
expect(semaphore.ubid).to start_with UBID::TYPE_SEMAPHORE
page = Page.create_with_id(summary: "x", tag: "y")
expect(page.ubid).to start_with UBID::TYPE_PAGE
end
# useful for comparing objects having network values
def string_kv(obj)
obj.to_hash.map { |k, v| [k.to_s, v.to_s] }.to_h
end
it "can decode ids" do
sshable = Sshable.create_with_id
host = VmHost.create(location: "x") { _1.id = sshable.id }
si = SpdkInstallation.create(version: "v1", allocation_weight: 100, vm_host_id: host.id) { _1.id = host.id }
vm = create_vm
dev = StorageDevice.create(name: "x", available_storage_gib: 1, total_storage_gib: 1, vm_host_id: host.id) { _1.id = host.id }
sv = VmStorageVolume.create_with_id(vm_id: vm.id, size_gib: 5, disk_index: 0, boot: false, spdk_installation_id: si.id, storage_device_id: dev.id)
kek = StorageKeyEncryptionKey.create_with_id(algorithm: "x", key: "x", init_vector: "x", auth_data: "x")
account = Account.create_with_id(email: "x@y.net")
project = account.create_project_with_default_policy("x")
st = SubjectTag.create_with_id(project_id: project.id, name: "T")
at = ActionTag.create_with_id(project_id: project.id, name: "T")
ot = ObjectTag.create_with_id(project_id: project.id, name: "T")
ace = AccessControlEntry.create_with_id(project_id: project.id, subject_id: st.id)
a_type = ActionType.first
atag = AccessTag.create_with_id(project_id: project.id, hyper_tag_table: "x", name: "x")
subnet = PrivateSubnet.create_with_id(net6: "0::0", net4: "127.0.0.1", name: "x", location: "x")
nic = Nic.create_with_id(private_ipv6: "fd10:9b0b:6b4b:8fbb::/128", private_ipv4: "10.0.0.12/32", mac: "00:11:22:33:44:55", encryption_key: "0x30613961313636632d653765372d343434372d616232392d376561343432623562623065", private_subnet_id: subnet.id, name: "def-nic")
tun = IpsecTunnel.create_with_id(src_nic_id: nic.id, dst_nic_id: nic.id)
adr = Address.create_with_id(cidr: "192.168.1.0/24", routed_to_host_id: host.id)
vm_adr = AssignedVmAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, dst_vm_id: vm.id)
host_adr = AssignedHostAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, host_id: host.id)
strand = Strand.create_with_id(prog: "x", label: "y")
semaphore = Semaphore.create_with_id(strand_id: strand.id, name: "z")
page = Page.create_with_id(summary: "x", tag: "y")
expect(described_class.decode(vm.ubid)).to eq(vm)
expect(described_class.decode(sv.ubid)).to eq(sv)
expect(described_class.decode(kek.ubid)).to eq(kek)
expect(described_class.decode(account.ubid)).to eq(account)
expect(described_class.decode(project.ubid)).to eq(project)
expect(described_class.decode(st.ubid)).to eq(st)
expect(described_class.decode(at.ubid)).to eq(at)
expect(described_class.decode(ot.ubid)).to eq(ot)
expect(described_class.decode(ace.ubid)).to eq(ace)
expect(described_class.decode(a_type.ubid)).to eq(a_type)
expect(described_class.decode(atag.ubid)).to eq(atag)
expect(described_class.decode(tun.ubid)).to eq(tun)
expect(string_kv(described_class.decode(subnet.ubid))).to eq(string_kv(subnet))
expect(described_class.decode(sshable.ubid)).to eq(sshable)
expect(string_kv(described_class.decode(adr.ubid))).to eq(string_kv(adr))
expect(string_kv(described_class.decode(vm_adr.ubid))).to eq(string_kv(vm_adr))
expect(string_kv(described_class.decode(host_adr.ubid))).to eq(string_kv(host_adr))
expect(described_class.decode(strand.ubid)).to eq(strand)
expect(described_class.decode(semaphore.ubid)).to eq(semaphore)
expect(described_class.decode(page.ubid)).to eq(page)
expect(string_kv(described_class.decode(nic.ubid))).to eq(string_kv(nic))
end
it "fails to decode unknown type" do
expect {
described_class.decode("han2sefsk4f61k91z77vn0y978")
}.to raise_error RuntimeError, "Couldn't decode ubid: han2sefsk4f61k91z77vn0y978"
end
it "can be inspected" do
expect(described_class.parse("vmqsknkzw5164hkfnt6z6zgjps").inspect).to eq("#<UBID:Vm @ubid=\"vmqsknkzw5164hkfnt6z6zgjps\" @uuid=\"be6759ff-8509-8b74-8cdf-5d1be6fc256c\">")
end
it ".resolve_map populates hash with uuid keys" do
page = Page.create_with_id(summary: "x", tag: "y")
a_type = ActionType.first
api_key = ApiKey.create(owner_table: "project", owner_id: Project.create(name: "test-project").id, used_for: "inference_endpoint", key: "1")
invalid = described_class.to_uuid("han2sefsk4f61k91z77vn0y978")
hash = {page.id => nil, a_type.id => nil, api_key.id => nil, invalid => nil}
described_class.resolve_map(hash)
expect(hash[page.id]).to eq page
expect(hash[a_type.id]).to eq a_type
expect(hash[api_key.id]).to eq api_key
expect(hash[invalid]).to be_nil
end
it ".type_match? checks whether given ubid has given type" do
ubid = ActionType.first.ubid
expect(described_class.type_match?(ubid, described_class::TYPE_ACTION_TYPE)).to be true
expect(described_class.type_match?(ubid, described_class::TYPE_ETC)).to be false
end
it ".uuid_type_match? checks whether given uuid has given type" do
uuid = ActionType.first.id
expect(described_class.uuid_type_match?(uuid, described_class::TYPE_ACTION_TYPE)).to be true
expect(described_class.uuid_type_match?(uuid, described_class::TYPE_ETC)).to be false
end
it "#type_match? checks whether receiver has given type" do
ubid = described_class.from_uuidish(ActionType.first.id)
expect(ubid.type_match?(described_class::TYPE_ACTION_TYPE)).to be true
expect(ubid.type_match?(described_class::TYPE_ETC)).to be false
end
it ".class_match? checks whether given ubid has given class" do
ubid = ActionType.first.ubid
expect(described_class.class_match?(ubid, ActionType)).to be true
expect(described_class.class_match?(ubid, ApiKey)).to be false
end
it ".uuid_class_match? checks whether given uuid has given class" do
uuid = ActionType.first.id
expect(described_class.uuid_class_match?(uuid, ActionType)).to be true
expect(described_class.uuid_class_match?(uuid, ApiKey)).to be false
end
it "#class_match? checks whether receiver has given class" do
ubid = described_class.from_uuidish(ActionType.first.id)
expect(ubid.class_match?(ActionType)).to be true
expect(ubid.class_match?(ApiKey)).to be false
end
end