cert_watch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +151 -0
  4. data/Rakefile +16 -0
  5. data/app/assets/javascripts/cert_watch/application.js +13 -0
  6. data/app/assets/stylesheets/cert_watch/application.css +15 -0
  7. data/app/controllers/cert_watch/application_controller.rb +5 -0
  8. data/app/jobs/cert_watch/install_certificate_job.rb +16 -0
  9. data/app/jobs/cert_watch/renew_certificate_job.rb +16 -0
  10. data/app/jobs/cert_watch/renew_expiring_certificates_job.rb +13 -0
  11. data/app/models/cert_watch/certificate.rb +42 -0
  12. data/app/views/layouts/cert_watch/application.html.erb +14 -0
  13. data/config/locales/de.yml +47 -0
  14. data/config/locales/en.yml +47 -0
  15. data/config/routes.rb +2 -0
  16. data/db/migrate/20160711193700_create_certificates.rb +18 -0
  17. data/lib/cert_watch/CHANGELOG.md +5 -0
  18. data/lib/cert_watch/certbot_client.rb +32 -0
  19. data/lib/cert_watch/client.rb +7 -0
  20. data/lib/cert_watch/configuration.rb +37 -0
  21. data/lib/cert_watch/domain_owner.rb +23 -0
  22. data/lib/cert_watch/engine.rb +10 -0
  23. data/lib/cert_watch/error.rb +4 -0
  24. data/lib/cert_watch/install_error.rb +4 -0
  25. data/lib/cert_watch/installer.rb +7 -0
  26. data/lib/cert_watch/pem_directory_installer.rb +55 -0
  27. data/lib/cert_watch/renew_error.rb +4 -0
  28. data/lib/cert_watch/sanitize.rb +13 -0
  29. data/lib/cert_watch/shell.rb +20 -0
  30. data/lib/cert_watch/version.rb +3 -0
  31. data/lib/cert_watch/views/all.rb +3 -0
  32. data/lib/cert_watch/views/certificate_state.rb +42 -0
  33. data/lib/cert_watch.rb +32 -0
  34. data/spec/cert_watch/certbot_client_spec.rb +53 -0
  35. data/spec/cert_watch/domain_owner_spec.rb +62 -0
  36. data/spec/cert_watch/pem_directory_installer_spec.rb +75 -0
  37. data/spec/cert_watch/sanitize_spec.rb +19 -0
  38. data/spec/cert_watch/shell_spec.rb +19 -0
  39. data/spec/cert_watch/views/certificate_state_spec.rb +33 -0
  40. data/spec/examples.txt +40 -0
  41. data/spec/factories/certificates.rb +6 -0
  42. data/spec/internal/config/database.yml +3 -0
  43. data/spec/internal/config/routes.rb +3 -0
  44. data/spec/internal/db/combustion_test.sqlite +0 -0
  45. data/spec/internal/db/schema.rb +7 -0
  46. data/spec/internal/log/jobs/test/cert_watch.log +3793 -0
  47. data/spec/internal/log/test.log +24187 -0
  48. data/spec/internal/public/favicon.ico +0 -0
  49. data/spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb +43 -0
  50. data/spec/models/cert_watch/certificate_spec.rb +102 -0
  51. data/spec/rails_helper.rb +45 -0
  52. data/spec/spec_helper.rb +98 -0
  53. data/spec/support/config/cert_watch.rb +7 -0
  54. data/spec/support/config/factory_girl.rb +11 -0
  55. data/spec/support/config/resque_logger.rb +16 -0
  56. data/spec/support/config/timecop.rb +21 -0
  57. data/spec/support/helpers/doubles.rb +28 -0
  58. data/spec/support/helpers/fixtures.rb +25 -0
  59. data/spec/support/helpers/inline_resque.rb +9 -0
  60. data/spec/support/helpers/view_component_example_group.rb +31 -0
  61. metadata +298 -0
@@ -0,0 +1,13 @@
1
+ module CertWatch
2
+ module Sanitize
3
+ extend self
4
+
5
+ class ForbiddenCharacters < Error; end
6
+
7
+ def check_domain!(name)
8
+ if name =~ /[^0-9a-zA-Z.-]/
9
+ fail(ForbiddenCharacters, "Domain '#{name}' contains forbidden characters.")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module CertWatch
2
+ module Shell
3
+ extend self
4
+
5
+ class CommandFailed < Error; end
6
+
7
+ def sudo(command)
8
+ output, input = IO.pipe
9
+ prefix = !Rails.env.test? ? 'sudo ' : ''
10
+ full_command = [prefix, command].join
11
+
12
+ result = system(full_command, [:out, :err] => input)
13
+ input.close
14
+
15
+ unless result
16
+ fail(CommandFailed, "Command '#{full_command}' failed with output:\n\n#{output.read}\n")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module CertWatch
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,3 @@
1
+ # View components cannot be discovered via the auto loader since they
2
+ # are never referenced by class name.
3
+ [CertWatch::Views::CertificateState]
@@ -0,0 +1,42 @@
1
+ require 'arbre'
2
+ require 'active_admin'
3
+
4
+ module CertWatch
5
+ module Views
6
+ class CertificateState < Arbre::Component
7
+ builder_method :cert_watch_certificate_state
8
+
9
+ STATE_MAPPING = {
10
+ 'installed' => 'ok',
11
+ 'installing' => 'warn',
12
+ 'renewing' => 'warn',
13
+ 'installing_failed' => 'error',
14
+ 'renewing_failed' => 'error'
15
+ }.freeze
16
+
17
+ def build(certificate_or_domain, options = {})
18
+ state = get_state(certificate_or_domain)
19
+ format = options.fetch(:format, 'short')
20
+
21
+ add_class 'cert_watch_certificate_state'
22
+
23
+ status_tag(t(state, scope: "cert_watch.states.#{format}"),
24
+ [state, STATE_MAPPING[state]].compact.join(' '))
25
+ end
26
+
27
+ private
28
+
29
+ def get_state(certificate_or_domain)
30
+ get_certificate(certificate_or_domain).try(:state) || 'not_found'
31
+ end
32
+
33
+ def get_certificate(certificate_or_domain)
34
+ if certificate_or_domain.is_a?(String)
35
+ Certificate.find_by_domain(certificate_or_domain)
36
+ else
37
+ certificate_or_domain
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/cert_watch.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'cert_watch/engine'
2
+
3
+ module CertWatch
4
+ def self.config
5
+ fail('Call CertWatch.setup before accessing CertWatch.config') unless @config
6
+ @config
7
+ end
8
+
9
+ def self.setup
10
+ @config = Configuration.new
11
+ yield @config if block_given?
12
+
13
+ self.client = CertbotClient.new(executable: config.certbot_executable,
14
+ port: config.certbot_port)
15
+
16
+ self.installer = PemDirectoryInstaller.new(pem_directory: config.pem_directory,
17
+ input_directory: config.certbot_output_directory,
18
+ reload_command: config.server_reload_command)
19
+ end
20
+
21
+ mattr_accessor :client
22
+
23
+ mattr_accessor :installer
24
+
25
+ def self.active_admin_load_path
26
+ Dir[CertWatch::Engine.root.join('admin')].first
27
+ end
28
+
29
+ def self.domain_owner(options)
30
+ DomainOwner.define(options)
31
+ end
32
+ end
@@ -0,0 +1,53 @@
1
+ require 'rails_helper'
2
+
3
+ module CertWatch
4
+ RSpec.describe CertbotClient do
5
+ let(:shell) do
6
+ instance_double('Shell', sudo: nil)
7
+ end
8
+
9
+ let(:client) do
10
+ CertbotClient.new(executable: '/usr/bin/certbot',
11
+ port: 99,
12
+ shell: shell)
13
+ end
14
+
15
+ it 'invokes given executable' do
16
+ client.renew('some.example.com')
17
+
18
+ expect(shell).to have_received(:sudo).with(a_string_including('/usr/bin/certbot'))
19
+ end
20
+
21
+ it 'passes given port' do
22
+ client.renew('some.example.com')
23
+
24
+ expect(shell).to have_received(:sudo).with(a_string_including('--http-01-port 99'))
25
+ end
26
+
27
+ it 'passes domain' do
28
+ client.renew('some.example.com')
29
+
30
+ expect(shell).to have_received(:sudo).with(a_string_including('-d some.example.com'))
31
+ end
32
+
33
+ it 'fails if domain contains forbidden characters' do
34
+ expect do
35
+ client.renew('some.example.com;" rm *')
36
+ end.to raise_error(Sanitize::ForbiddenCharacters)
37
+ end
38
+
39
+ it 'passes --renew-by-default flag' do
40
+ client.renew('some.example.com')
41
+
42
+ expect(shell).to have_received(:sudo).with(a_string_including('--renew-by-default'))
43
+ end
44
+
45
+ it 'fails with RenewError if shell command fails' do
46
+ allow(shell).to receive(:sudo).and_raise(Shell::CommandFailed)
47
+
48
+ expect do
49
+ client.renew('some.example.com')
50
+ end.to raise_error(RenewError)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ require 'rails_helper'
2
+
3
+ require 'support/helpers/doubles'
4
+
5
+ module CertWatch
6
+ RSpec.describe DomainOwner do
7
+ let(:test_domain_owner_model) do
8
+ Class.new(ActiveRecord::Base) do
9
+ self.table_name = :test_domain_owner
10
+ include CertWatch.domain_owner(attribute: :cname)
11
+ end
12
+ end
13
+
14
+ it 'renews certificate on create' do
15
+ test_domain_owner_model.create!(cname: 'new.example.com')
16
+
17
+ certificate = Certificate.find_by_domain('new.example.com')
18
+
19
+ expect(certificate).to be_present
20
+ expect(certificate.state).to eq('renewing')
21
+ end
22
+
23
+ it 'renews certificate on update' do
24
+ domain_owner = test_domain_owner_model.create!(cname: 'old.example.com')
25
+
26
+ domain_owner.update!(cname: 'new.example.com')
27
+ certificate = Certificate.find_by_domain('new.example.com')
28
+
29
+ expect(certificate).to be_present
30
+ expect(certificate.state).to eq('renewing')
31
+ end
32
+
33
+ it 'does not renew certificate when cname is unchanged' do
34
+ domain_owner = test_domain_owner_model.create!(name: 'Old', cname: 'some.example.com')
35
+ certificate = Certificate.find_by_domain!('some.example.com')
36
+ certificate.update!(state: 'installed')
37
+
38
+ domain_owner.update!(name: 'New')
39
+
40
+ expect(certificate.reload.state).to eq('installed')
41
+ end
42
+
43
+ it 'does not create certificate when new cname is blank' do
44
+ domain_owner = test_domain_owner_model.create!(name: 'Old', cname: 'some.example.com')
45
+ certificate = Certificate.find_by_domain!('some.example.com')
46
+ certificate.update!(state: 'installed')
47
+
48
+ expect do
49
+ domain_owner.update!(cname: '')
50
+ end.not_to change { Certificate.count }
51
+ end
52
+
53
+ it 'abandons old certificate on update' do
54
+ domain_owner = test_domain_owner_model.create!(cname: 'old.example.com')
55
+
56
+ domain_owner.update!(cname: 'new.example.com')
57
+ certificate = Certificate.find_by_domain('old.example.com')
58
+
59
+ expect(certificate.state).to eq('abandoned')
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ require 'rails_helper'
2
+
3
+ module CertWatch
4
+ RSpec.describe PemDirectoryInstaller, fixture_files: true do
5
+ before do
6
+ Fixtures.file('live/some.example.com/fullchain.pem', "FULL CHAIN\n")
7
+ Fixtures.file('live/some.example.com/privkey.pem', "PRIVATE KEY\n")
8
+ Fixtures.directory('ssl')
9
+ end
10
+
11
+ it 'concatenates full chain and private key files' do
12
+ installer = PemDirectoryInstaller.new(input_directory: 'live',
13
+ pem_directory: 'ssl',
14
+ reload_command: 'touch reload.txt')
15
+ installer.install('some.example.com')
16
+
17
+ expect(File.read('ssl/some.example.com.pem')).to eq("FULL CHAIN\nPRIVATE KEY\n")
18
+ end
19
+
20
+ it 'invokes reload command' do
21
+ installer = PemDirectoryInstaller.new(input_directory: 'live',
22
+ pem_directory: 'ssl',
23
+ reload_command: 'touch reload.txt')
24
+ installer.install('some.example.com')
25
+
26
+ expect(File.exist?('reload.txt')).to eq(true)
27
+ end
28
+
29
+ it 'fails with InstallError if reload command fails' do
30
+ installer = PemDirectoryInstaller.new(input_directory: 'live',
31
+ pem_directory: 'ssl',
32
+ reload_command: './not_there')
33
+ expect do
34
+ installer.install('some.example.com')
35
+ end.to raise_error(InstallError)
36
+ end
37
+
38
+ it 'fails with InstallError if input files not found' do
39
+ installer = PemDirectoryInstaller.new(input_directory: 'live',
40
+ pem_directory: 'ssl')
41
+ expect do
42
+ installer.install('not-there.example.com')
43
+ end.to raise_error(InstallError)
44
+ end
45
+
46
+ it 'does not create output file if input files not found' do
47
+ installer = PemDirectoryInstaller.new(input_directory: 'live',
48
+ pem_directory: 'ssl')
49
+
50
+ begin
51
+ installer.install('not-there.example.com')
52
+ rescue InstallError
53
+ end
54
+
55
+ expect(File.exist?('ssl/not-there.example.com.pem')).to eq(false)
56
+ end
57
+
58
+ it 'fails with InstallError if output directory does not exist' do
59
+ installer = PemDirectoryInstaller.new(input_directory: 'live',
60
+ pem_directory: 'not-there')
61
+ expect do
62
+ installer.install('some.example.com')
63
+ end.to raise_error(InstallError)
64
+ end
65
+
66
+ it 'fails if domain contains forbidden characters' do
67
+ installer = PemDirectoryInstaller.new(input_directory: 'live',
68
+ pem_directory: 'ssl')
69
+
70
+ expect do
71
+ installer.install('some.*example ".com')
72
+ end.to raise_error(Sanitize::ForbiddenCharacters)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails_helper'
2
+
3
+ module CertWatch
4
+ RSpec.describe Sanitize do
5
+ describe '.check_domain!' do
6
+ it 'fails if string contains forbidden characters' do
7
+ expect do
8
+ Sanitize.check_domain!('some.12-; rm *"')
9
+ end.to raise_error(Sanitize::ForbiddenCharacters)
10
+ end
11
+
12
+ it 'handles multi line strings' do
13
+ expect do
14
+ Sanitize.check_domain!("valid\n rm *")
15
+ end.to raise_error(Sanitize::ForbiddenCharacters)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails_helper'
2
+
3
+ module CertWatch
4
+ RSpec.describe Shell, fixture_files: true do
5
+ describe '.sudo' do
6
+ it 'runs given command' do
7
+ Shell.sudo('echo "test" > foo')
8
+
9
+ expect(File.read('foo')).to eq("test\n")
10
+ end
11
+
12
+ it 'raises CommandFailed with output if command fails' do
13
+ expect do
14
+ Shell.sudo('LANG=en touch not/there')
15
+ end.to raise_error(Shell::CommandFailed, /cannot touch/)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ require 'rails_helper'
2
+
3
+ module CertWatch
4
+ module Views
5
+ RSpec.describe CertificateState, type: :view_component do
6
+ context 'with string argument' do
7
+ it 'renders status tag for matching certificate' do
8
+ create(:certificate, state: 'installed', domain: 'my.example.com')
9
+
10
+ render(:cert_watch_certificate_state, 'my.example.com')
11
+
12
+ expect(rendered).to have_selector('.status_tag.installed')
13
+ end
14
+
15
+ it 'renders status tag if certificate is not found' do
16
+ render(:cert_watch_certificate_state, 'not.there.com')
17
+
18
+ expect(rendered).to have_selector('.status_tag.not_found')
19
+ end
20
+ end
21
+
22
+ context 'with certificate argument' do
23
+ it 'renders status tag for certificate' do
24
+ certificate = create(:certificate, state: 'installed', domain: 'my.example.com')
25
+
26
+ render(:cert_watch_certificate_state, certificate)
27
+
28
+ expect(rendered).to have_selector('.status_tag.installed')
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/spec/examples.txt ADDED
@@ -0,0 +1,40 @@
1
+ example_id | status | run_time |
2
+ ------------------------------------------------------------------- | ------ | --------------- |
3
+ ./spec/cert_watch/certbot_client_spec.rb[1:1] | passed | 0.00682 seconds |
4
+ ./spec/cert_watch/certbot_client_spec.rb[1:2] | passed | 0.00204 seconds |
5
+ ./spec/cert_watch/certbot_client_spec.rb[1:3] | passed | 0.00203 seconds |
6
+ ./spec/cert_watch/certbot_client_spec.rb[1:4] | passed | 0.00201 seconds |
7
+ ./spec/cert_watch/certbot_client_spec.rb[1:5] | passed | 0.00223 seconds |
8
+ ./spec/cert_watch/certbot_client_spec.rb[1:6] | passed | 0.0029 seconds |
9
+ ./spec/cert_watch/domain_owner_spec.rb[1:1] | passed | 0.04831 seconds |
10
+ ./spec/cert_watch/domain_owner_spec.rb[1:2] | passed | 0.07414 seconds |
11
+ ./spec/cert_watch/domain_owner_spec.rb[1:3] | passed | 0.06987 seconds |
12
+ ./spec/cert_watch/domain_owner_spec.rb[1:4] | passed | 0.08508 seconds |
13
+ ./spec/cert_watch/domain_owner_spec.rb[1:5] | passed | 0.08109 seconds |
14
+ ./spec/cert_watch/pem_directory_installer_spec.rb[1:1] | passed | 0.03524 seconds |
15
+ ./spec/cert_watch/pem_directory_installer_spec.rb[1:2] | passed | 0.05051 seconds |
16
+ ./spec/cert_watch/pem_directory_installer_spec.rb[1:3] | passed | 0.03625 seconds |
17
+ ./spec/cert_watch/pem_directory_installer_spec.rb[1:4] | passed | 0.01778 seconds |
18
+ ./spec/cert_watch/pem_directory_installer_spec.rb[1:5] | passed | 0.02905 seconds |
19
+ ./spec/cert_watch/pem_directory_installer_spec.rb[1:6] | passed | 0.02549 seconds |
20
+ ./spec/cert_watch/pem_directory_installer_spec.rb[1:7] | passed | 0.00437 seconds |
21
+ ./spec/cert_watch/sanitize_spec.rb[1:1:1] | passed | 0.00192 seconds |
22
+ ./spec/cert_watch/sanitize_spec.rb[1:1:2] | passed | 0.00219 seconds |
23
+ ./spec/cert_watch/shell_spec.rb[1:1:1] | passed | 0.01215 seconds |
24
+ ./spec/cert_watch/shell_spec.rb[1:1:2] | passed | 0.01372 seconds |
25
+ ./spec/cert_watch/views/certificate_state_spec.rb[1:1:1] | passed | 0.0096 seconds |
26
+ ./spec/cert_watch/views/certificate_state_spec.rb[1:1:2] | passed | 0.00646 seconds |
27
+ ./spec/cert_watch/views/certificate_state_spec.rb[1:2:1] | passed | 0.03833 seconds |
28
+ ./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:1] | passed | 0.01287 seconds |
29
+ ./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:2] | passed | 0.0086 seconds |
30
+ ./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:3] | passed | 0.0154 seconds |
31
+ ./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:4] | passed | 0.01655 seconds |
32
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:1] | passed | 0.01814 seconds |
33
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:2] | passed | 0.03732 seconds |
34
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:3] | passed | 0.01962 seconds |
35
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:4] | passed | 0.02037 seconds |
36
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:5] | passed | 0.01938 seconds |
37
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:6:1] | passed | 0.01556 seconds |
38
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:6:2] | passed | 0.01882 seconds |
39
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:7:1] | passed | 0.02004 seconds |
40
+ ./spec/models/cert_watch/certificate_spec.rb[1:1:7:2] | passed | 0.02153 seconds |
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ factory(:certificate, class: CertWatch::Certificate) do
3
+ domain 'some.example.com'
4
+ state 'not_installed'
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/combustion_test.sqlite
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ #
3
+ end
@@ -0,0 +1,7 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table(:test_domain_owner, force: true) do |t|
3
+ t.string :name
4
+ t.string :cname
5
+ t.timestamps null: false
6
+ end
7
+ end