For the includers of HyperTagMethods, this changes the authorization code and object_tag member validation code to look at the project_id column for the object, instead of looking a row with the project and object in the access_tag table. This removes all calls to associate_with_project, other than those for Account. It removes the projects association for the includers of HyperTagMethods, and adds a project association to the models that didn't already have one, since there is only a single project for each object now. Most HyperTagMethods code is inlined into Account, since it is only user of the code now. Temporarily, other models will still include HyperTagMethods for the before_destroy hook, but eventually it will go away completely. The associations in Projects that previous used access_tag as a join table, are changed from many_to_many to one_to_many, except for Account (which still uses the join table). Project#has_resources now needs separate queries for all of the resource classes to see if there any associated objects. This causes a lot of fallout in the specs, but unfortunately that is unavoidable due the extensive use of projects.first in the specs to get the related project for the objects, as well as the extensive use of associate_with_project.
337 lines
16 KiB
Ruby
337 lines
16 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe Prog::Vnet::CertNexus do
|
|
subject(:nx) {
|
|
described_class.new(st)
|
|
}
|
|
|
|
let(:st) { Strand.new }
|
|
let(:project) {
|
|
Project.create_with_id(name: "test-prj")
|
|
}
|
|
let(:dns_zone) {
|
|
DnsZone.create_with_id(name: "test-dns-zone", project_id: project.id)
|
|
}
|
|
let(:cert) {
|
|
described_class.assemble("cert-hostname", dns_zone.id).subject
|
|
}
|
|
|
|
before do
|
|
allow(nx).to receive(:cert).and_return(cert)
|
|
end
|
|
|
|
describe ".assemble" do
|
|
it "creates a new certificate" do
|
|
st = described_class.assemble("test-hostname", dns_zone.id)
|
|
expect(Cert[st.id].hostname).to eq "test-hostname"
|
|
expect(st.label).to eq "start"
|
|
end
|
|
|
|
it "fails if dns_zone is not valid" do
|
|
id = SecureRandom.uuid
|
|
expect {
|
|
described_class.assemble("test-hostname", id)
|
|
}.to raise_error RuntimeError, "Given DNS zone doesn't exist with the id #{id}"
|
|
end
|
|
end
|
|
|
|
describe "#before_run" do
|
|
it "hops to destroy when needed" do
|
|
expect(nx).to receive(:when_destroy_set?).and_yield
|
|
expect { nx.before_run }.to hop("destroy")
|
|
end
|
|
|
|
it "does not hop to destroy if already in the destroy state" do
|
|
expect(nx).to receive(:when_destroy_set?).and_yield
|
|
expect(nx).to receive(:strand).and_return(Strand.new(label: "destroy"))
|
|
expect { nx.before_run }.not_to hop
|
|
end
|
|
end
|
|
|
|
describe "#start" do
|
|
let(:order) {
|
|
dns_challenge = instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name", record_type: "test-record-type", record_content: "test-record-content")
|
|
authorization = instance_double(Acme::Client::Resources::Authorization, dns: dns_challenge)
|
|
instance_double(Acme::Client::Resources::Order, authorizations: [authorization], url: "test-order-url")
|
|
}
|
|
|
|
it "registers a deadline and starts the certificate creation process" do
|
|
client = instance_double(Acme::Client)
|
|
key = Clec::Cert.ec_key
|
|
expect(OpenSSL::PKey::EC).to receive(:generate).with("prime256v1").and_return(key)
|
|
expect(Acme::Client).to receive(:new).with(private_key: key, directory: Config.acme_directory).and_return(client)
|
|
expect(client).to receive(:new_account).with(contact: "mailto:#{Config.acme_email}", terms_of_service_agreed: true, external_account_binding: {kid: Config.acme_eab_kid, hmac_key: Config.acme_eab_hmac_key}).and_return(instance_double(Acme::Client::Resources::Account, kid: "test-kid"))
|
|
expect(client).to receive(:new_order).with(identifiers: [cert.hostname]).and_return(order)
|
|
expect(cert).to receive(:update).with(kid: "test-kid", account_key: key.to_der, order_url: "test-order-url")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name"))
|
|
expect(dns_zone).to receive(:insert_record).with(record_name: "test-record-name.cert-hostname", type: "test-record-type", ttl: 600, data: "test-record-content")
|
|
|
|
expect { nx.start }.to hop("wait_dns_update")
|
|
end
|
|
|
|
it "creates a self-signed certificate in development environments without dns" do
|
|
expect(Config).to receive(:development?).and_return(true)
|
|
expect(cert).to receive(:dns_zone_id).and_return(nil)
|
|
|
|
expect { nx.start }.to hop("wait")
|
|
end
|
|
end
|
|
|
|
describe "#wait_dns_update" do
|
|
it "waits for dns_record to be seen by all servers" do
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name", record_content: "content")).at_least(:once)
|
|
dns_record = instance_double(DnsRecord, id: SecureRandom.uuid)
|
|
expect(DnsRecord).to receive(:[]).with(dns_zone_id: dns_zone.id, name: "test-record-name.cert-hostname.", tombstoned: false, data: "content").and_return(dns_record)
|
|
expect { nx.wait_dns_update }.to nap(10)
|
|
end
|
|
|
|
it "requests validation when dns_record is seen by all servers" do
|
|
challenge = instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name", record_content: "content")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(nx).to receive(:dns_challenge).and_return(challenge).at_least(:once)
|
|
dns_record = DnsRecord.create_with_id(dns_zone_id: dns_zone.id, name: "test-record-name.cert-hostname.", type: "test-record-type", ttl: 600, data: "content")
|
|
DB["INSERT INTO seen_dns_records_by_dns_servers(dns_record_id, dns_server_id) VALUES('#{dns_record.id}', NULL)"].insert
|
|
|
|
expect(challenge).to receive(:request_validation)
|
|
expect { nx.wait_dns_update }.to hop("wait_dns_validation")
|
|
end
|
|
end
|
|
|
|
describe "#wait_dns_validation" do
|
|
let(:challenge) {
|
|
instance_double(Acme::Client::Resources::Challenges::DNS01, status: "pending", record_name: "test-record-name", record_content: "content")
|
|
}
|
|
|
|
before do
|
|
expect(nx).to receive(:dns_challenge).and_return(challenge).at_least(:once)
|
|
end
|
|
|
|
it "waits for dns_challenge to be validated" do
|
|
expect { nx.wait_dns_validation }.to nap(10)
|
|
end
|
|
|
|
it "returns back to start if dns_challenge validation fails" do
|
|
expect(challenge).to receive(:status).and_return("failed")
|
|
expect(Clog).to receive(:emit).with("DNS validation failed")
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect { nx.wait_dns_validation }.to hop("start")
|
|
end
|
|
|
|
it "finalizes the certificate when dns_challenge is valid" do
|
|
expect(challenge).to receive(:status).and_return("valid")
|
|
|
|
key = Clec::Cert.ec_key
|
|
expect(OpenSSL::PKey::EC).to receive(:generate).and_return(key)
|
|
csr = instance_double(Acme::Client::CertificateRequest)
|
|
acme_order = instance_double(Acme::Client::Resources::Order)
|
|
expect(nx).to receive(:acme_order).and_return(acme_order).at_least(:once)
|
|
expect(Acme::Client::CertificateRequest).to receive(:new).with(private_key: key, common_name: "cert-hostname").and_return(csr)
|
|
expect(acme_order).to receive(:finalize).with(csr: csr)
|
|
expect(cert).to receive(:update).with(csr_key: key.to_der)
|
|
expect { nx.wait_dns_validation }.to hop("wait_cert_finalization")
|
|
end
|
|
end
|
|
|
|
describe "#wait_cert_finalization" do
|
|
let(:acme_order) {
|
|
instance_double(Acme::Client::Resources::Order, status: "processing")
|
|
}
|
|
|
|
before do
|
|
expect(nx).to receive(:acme_order).and_return(acme_order).at_least(:once)
|
|
end
|
|
|
|
it "waits for certificate to be finalized" do
|
|
expect { nx.wait_cert_finalization }.to nap(10)
|
|
end
|
|
|
|
it "returns back to start if certificate finalization fails" do
|
|
challenge = instance_double(Acme::Client::Resources::Challenges::DNS01, status: "pending", record_name: "test-record-name", record_content: "content")
|
|
expect(nx).to receive(:dns_challenge).and_return(challenge).at_least(:once)
|
|
expect(acme_order).to receive(:status).and_return("failed")
|
|
expect(Clog).to receive(:emit).with("Certificate finalization failed")
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect { nx.wait_cert_finalization }.to hop("start")
|
|
end
|
|
|
|
it "updates the certificate when certificate is valid" do
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name"))
|
|
expect(acme_order).to receive(:status).and_return("valid")
|
|
expect(acme_order).to receive(:certificate).and_return("test-certificate")
|
|
expect(Time).to receive(:now).and_return(Time.new(2021, 1, 1, 0, 0, 0))
|
|
expect(cert).to receive(:update).with(cert: "test-certificate", created_at: Time.new(2021, 1, 1, 0, 0, 0))
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
expect { nx.wait_cert_finalization }.to hop("wait")
|
|
end
|
|
end
|
|
|
|
describe "#wait" do
|
|
it "waits for 1 month" do
|
|
expect(cert).to receive(:created_at).and_return(Time.new(2021, 4, 1, 0, 0, 0))
|
|
expect(Time).to receive(:now).and_return(Time.new(2021, 4, 1, 0, 0, 0))
|
|
expect { nx.wait }.to nap(60 * 60 * 24 * 30 * 1)
|
|
end
|
|
|
|
it "destroys the certificate after 3 months" do
|
|
created_at = Time.new(2021, 1, 1, 0, 0, 0)
|
|
expect(cert).to receive(:created_at).and_return(created_at)
|
|
expect(Time).to receive(:now).and_return(created_at + 60 * 60 * 24 * 30 * 3 + 1)
|
|
expect(cert).to receive(:incr_destroy)
|
|
expect { nx.wait }.to nap(0)
|
|
end
|
|
end
|
|
|
|
describe "#destroy" do
|
|
it "revokes the certificate and deletes the dns record" do
|
|
client = instance_double(Acme::Client)
|
|
expect(cert).to receive(:cert).and_return("test-cert").at_least(:once)
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name")).at_least(:once)
|
|
expect(nx).to receive(:acme_client).and_return(client)
|
|
expect(client).to receive(:revoke).with(certificate: "test-cert", reason: "cessationOfOperation")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
|
|
expect { nx.destroy }.to exit({"msg" => "certificate revoked and destroyed"})
|
|
end
|
|
|
|
it "does not revoke the certificate if it doesn't exist" do
|
|
expect(cert).to receive(:cert).and_return(nil)
|
|
expect(nx).not_to receive(:acme_client)
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name")).at_least(:once)
|
|
|
|
expect { nx.destroy }.to exit({"msg" => "certificate revoked and destroyed"})
|
|
end
|
|
|
|
it "skips deleting the dns record if dns_challenge doesn't exist" do
|
|
expect(cert).to receive(:cert).and_return(nil)
|
|
expect(nx).to receive(:dns_challenge).and_return(nil)
|
|
|
|
expect { nx.destroy }.to exit({"msg" => "certificate revoked and destroyed"})
|
|
end
|
|
|
|
it "skips revocation and dns record deletion for self-signed certificates" do
|
|
expect(Config).to receive(:development?).and_return(true)
|
|
expect(cert).to receive(:dns_zone_id).and_return(nil)
|
|
expect(cert).to receive(:destroy)
|
|
|
|
expect { nx.destroy }.to exit({"msg" => "self-signed certificate destroyed"})
|
|
end
|
|
|
|
it "emits a log and continues if the cert is already revoked" do
|
|
client = instance_double(Acme::Client)
|
|
expect(cert).to receive(:cert).and_return("test-cert").at_least(:once)
|
|
expect(nx).to receive(:acme_client).and_return(client)
|
|
expect(client).to receive(:revoke).and_raise(Acme::Client::Error::AlreadyRevoked.new("already revoked"))
|
|
|
|
expect(Clog).to receive(:emit).with("Certificate is already revoked")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name")).at_least(:once)
|
|
expect(cert).to receive(:destroy)
|
|
|
|
expect { nx.destroy }.to exit({"msg" => "certificate revoked and destroyed"})
|
|
end
|
|
|
|
it "emits a log and continues if the cert is not found" do
|
|
client = instance_double(Acme::Client)
|
|
expect(cert).to receive(:cert).and_return("test-cert").at_least(:once)
|
|
expect(nx).to receive(:acme_client).and_return(client)
|
|
expect(client).to receive(:revoke).and_raise(Acme::Client::Error::NotFound.new("not found"))
|
|
|
|
expect(Clog).to receive(:emit).with("Certificate is not found")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name")).at_least(:once)
|
|
expect(cert).to receive(:destroy)
|
|
|
|
expect { nx.destroy }.to exit({"msg" => "certificate revoked and destroyed"})
|
|
end
|
|
|
|
it "emits a log and continues if the cert is revoked previously and we get Unauthorized" do
|
|
client = instance_double(Acme::Client)
|
|
expect(cert).to receive(:cert).and_return("test-cert").at_least(:once)
|
|
expect(nx).to receive(:acme_client).and_return(client)
|
|
expect(client).to receive(:revoke).and_raise(Acme::Client::Error::Unauthorized.new("The certificate has expired and cannot be revoked"))
|
|
|
|
expect(Clog).to receive(:emit).with("Certificate is expired and cannot be revoked")
|
|
expect(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
expect(dns_zone).to receive(:delete_record).with(record_name: "test-record-name.cert-hostname")
|
|
expect(nx).to receive(:dns_challenge).and_return(instance_double(Acme::Client::Resources::Challenges::DNS01, record_name: "test-record-name")).at_least(:once)
|
|
expect(cert).to receive(:destroy)
|
|
|
|
expect { nx.destroy }.to exit({"msg" => "certificate revoked and destroyed"})
|
|
end
|
|
|
|
it "fires an exception if the cert is not authorized and the message is not about the certificate being expired" do
|
|
client = instance_double(Acme::Client)
|
|
expect(cert).to receive(:cert).and_return("test-cert").at_least(:once)
|
|
expect(nx).to receive(:acme_client).and_return(client)
|
|
expect(client).to receive(:revoke).and_raise(Acme::Client::Error::Unauthorized.new("The certificate is not authorized"))
|
|
|
|
expect { nx.destroy }.to raise_error(Acme::Client::Error::Unauthorized)
|
|
end
|
|
end
|
|
|
|
describe "#acme_client" do
|
|
it "returns a new acme client" do
|
|
expect(cert).to receive(:account_key).and_return("test-account-key").at_least(:once)
|
|
expect(cert).to receive(:kid).and_return("test-kid")
|
|
expect(OpenSSL::PKey::EC).to receive(:new).with("test-account-key").and_return("account-key")
|
|
expect(Acme::Client).to receive(:new).with(private_key: "account-key", directory: Config.acme_directory, kid: "test-kid").and_return("client")
|
|
|
|
expect(nx.acme_client).to eq "client"
|
|
end
|
|
|
|
it "returns nil if account key is not set" do
|
|
expect(cert).to receive(:account_key).and_return(nil)
|
|
|
|
expect(nx.acme_client).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "#acme_order" do
|
|
it "returns the acme order" do
|
|
expect(cert).to receive(:order_url).and_return("test-order-url").at_least(:once)
|
|
client = instance_double(Acme::Client)
|
|
expect(nx).to receive(:acme_client).and_return(client)
|
|
expect(client).to receive(:order).with(url: "test-order-url").and_return("order")
|
|
|
|
expect(nx.acme_order).to eq "order"
|
|
end
|
|
|
|
it "returns nil if order_url is nil" do
|
|
expect(cert).to receive(:order_url).and_return(nil)
|
|
expect(nx.acme_order).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "#dns_challenge" do
|
|
it "returns the dns challenge" do
|
|
order = instance_double(Acme::Client::Resources::Order)
|
|
expect(nx).to receive(:acme_order).and_return(order)
|
|
expect(order).to receive(:authorizations).and_return([instance_double(Acme::Client::Resources::Authorization, dns: "dns")])
|
|
|
|
expect(nx.dns_challenge).to eq "dns"
|
|
end
|
|
end
|
|
|
|
describe "#dns_zone" do
|
|
it "returns the dns zone" do
|
|
expect(DnsZone).to receive(:[]).with(cert.dns_zone_id).and_return("dns-zone")
|
|
expect(nx.dns_zone).to eq "dns-zone"
|
|
end
|
|
|
|
it "returns nil if dns_zone_id is not set" do
|
|
expect(cert).to receive(:dns_zone_id).and_return(nil)
|
|
expect(nx.dns_zone).to be_nil
|
|
end
|
|
end
|
|
end
|