cert_watch 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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