vagrant-docker-certificates-manager 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/vagrant-docker-certificates-manager/VERSION +1 -1
- data/lib/vagrant-docker-certificates-manager/actions/install.rb +15 -3
- data/lib/vagrant-docker-certificates-manager/actions/uninstall.rb +44 -3
- data/lib/vagrant-docker-certificates-manager/config.rb +59 -11
- data/lib/vagrant-docker-certificates-manager/helpers.rb +78 -50
- data/lib/vagrant-docker-certificates-manager/util/cert.rb +94 -0
- data/lib/vagrant-docker-certificates-manager/util/os.rb +136 -50
- data/lib/vagrant-docker-certificates-manager/util/registry.rb +52 -1
- data/lib/vagrant-docker-certificates-manager/version.rb +7 -5
- data/locales/en.yml +0 -11
- data/locales/fr.yml +0 -11
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7205874687ebbce5453b9267447c8a007d949416b370e67d141b4e633193ce1b
|
|
4
|
+
data.tar.gz: 5970d32848bbb46f63f4d3b1dd0c12823350937e69363dbd1c4e1d7e7cf1ab16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da2f09201144ccaa74456e248c84d3c89297be73764c7d7cd2cc5a5fda5547c08b03d1eb9099fdab2808bf7a51e392faab5496ed3033ef86a4e96c9121388e88
|
|
7
|
+
data.tar.gz: 8a8f01b14910172242e72b62bd9dd889fcc79f56e0449f2d48ccffd763c91f5dab7a887ca52b214efdd8038d976c7e0c9464d4069b95c03ecd226fa1f30ebefd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/julienpoirou/vagrant-docker-certificates-manager/compare/v0.3.0...v0.4.0) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fonctionnalités ✨
|
|
7
|
+
|
|
8
|
+
* Improve plugin quality and publish RubyDoc ([278298b](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/278298b614ac9e4b0797d806bed3e0015d7fdb1f))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Corrections 🐛
|
|
12
|
+
|
|
13
|
+
* Resolve rubocop offenses and enable pages for RubyDoc deploy ([227c596](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/227c5967c1480a2771413cf5738d2ac1f030d495))
|
|
14
|
+
|
|
15
|
+
## [0.3.0](https://github.com/julienpoirou/vagrant-docker-certificates-manager/compare/v0.2.0...v0.3.0) (2026-03-09)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fonctionnalités ✨
|
|
19
|
+
|
|
20
|
+
* **firefox:** Adding certificate management to Firefox ([6e067c3](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/6e067c3a99f7963aa27b8cfc7b1e481a443af4ec))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Corrections 🐛
|
|
24
|
+
|
|
25
|
+
* **rubocop:** Migrate rubocop-rspec from require to plugins ([9a22642](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/9a226421d98aab0d781fd1ac3d0845e5b1c43ff4))
|
|
26
|
+
* **rubocop:** Restore explicit *args for Ruby 3.1 compatibility ([595438d](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/595438d061e2f1aafb7616b91110935ef4718a5c))
|
|
27
|
+
|
|
3
28
|
## [0.2.0](https://github.com/julienpoirou/vagrant-docker-certificates-manager/compare/v0.1.0...v0.2.0) (2025-08-20)
|
|
4
29
|
|
|
5
30
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.0
|
|
@@ -24,6 +24,15 @@ module VagrantDockerCertificatesManager
|
|
|
24
24
|
@app.call(env)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Installs a configured certificate into the current host trust store.
|
|
28
|
+
#
|
|
29
|
+
# The operation is idempotent: when the fingerprint is already tracked,
|
|
30
|
+
# the current machine adopts the existing registry entry instead of
|
|
31
|
+
# reinstalling the certificate.
|
|
32
|
+
#
|
|
33
|
+
# @param cfg [#cert_path, #cert_name] Vagrant certificate configuration.
|
|
34
|
+
# @param env [Hash, nil] Vagrant environment hash.
|
|
35
|
+
# @return [Hash] Normalized result with code, status, data, or error.
|
|
27
36
|
def self.perform_install(cfg, env)
|
|
28
37
|
unless File.file?(cfg.cert_path)
|
|
29
38
|
return { code: 1, status: "error",
|
|
@@ -32,9 +41,11 @@ error: UiHelpers.t("errors.invalid_path", path: cfg.cert_path) }
|
|
|
32
41
|
|
|
33
42
|
name = cfg.cert_name.to_s.strip.empty? ? Cert.default_name_from(cfg.cert_path) : cfg.cert_name
|
|
34
43
|
fp = Cert.sha1(cfg.cert_path)
|
|
44
|
+
mid = env && env[:machine]&.id
|
|
45
|
+
|
|
35
46
|
if Registry.all.key?(fp)
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
Registry.adopt(fp, mid) if mid
|
|
48
|
+
return { code: 0, status: "success", data: { os: OS.detect, cert: name, already: true } }
|
|
38
49
|
end
|
|
39
50
|
|
|
40
51
|
os = OS.detect
|
|
@@ -51,7 +62,8 @@ firefox: cfg.manage_firefox)
|
|
|
51
62
|
"path" => File.expand_path(cfg.cert_path),
|
|
52
63
|
"name" => name,
|
|
53
64
|
"nickname" => Cert.nickname_for(name),
|
|
54
|
-
"os" => os.to_s
|
|
65
|
+
"os" => os.to_s,
|
|
66
|
+
"owners" => [mid].compact
|
|
55
67
|
})
|
|
56
68
|
{ code: 0, status: "success", data: { os: os, cert: name } }
|
|
57
69
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require_relative "../util/os"
|
|
4
5
|
require_relative "../util/ui"
|
|
5
6
|
require_relative "../util/cert"
|
|
7
|
+
require_relative "../util/generator"
|
|
6
8
|
require_relative "../util/registry"
|
|
7
9
|
require_relative "../helpers"
|
|
8
10
|
|
|
@@ -12,7 +14,7 @@ module VagrantDockerCertificatesManager
|
|
|
12
14
|
def initialize(app, env); @app = app; @env = env; end
|
|
13
15
|
|
|
14
16
|
def call(env)
|
|
15
|
-
cfg = env[:machine].config.
|
|
17
|
+
cfg = env[:machine].config.docker_certificates
|
|
16
18
|
UiHelpers.set_locale!(cfg.locale || "en")
|
|
17
19
|
if cfg.remove_on_destroy
|
|
18
20
|
Ui.say(env, :info, "uninstall.start", name: cfg.cert_name)
|
|
@@ -21,22 +23,61 @@ module VagrantDockerCertificatesManager
|
|
|
21
23
|
result[:status] == "success" ? "uninstall.success" : "uninstall.fail",
|
|
22
24
|
name: cfg.cert_name)
|
|
23
25
|
end
|
|
26
|
+
purge_generated(cfg, env)
|
|
24
27
|
@app.call(env)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def purge_generated(cfg, env)
|
|
31
|
+
# Generated material is purged only through explicit env flags to avoid
|
|
32
|
+
# deleting a CA/server certificate that may be shared outside this action.
|
|
33
|
+
files = []
|
|
34
|
+
files += [Generator::CA_CERT, Generator::CA_KEY, Generator::CRL] if env_flag?("VDCM_PURGE_CA_ON_DESTROY")
|
|
35
|
+
files += [Generator::SRV_CRT, Generator::SRV_KEY] if env_flag?("VDCM_PURGE_SERVER_ON_DESTROY")
|
|
36
|
+
return if files.empty?
|
|
37
|
+
|
|
38
|
+
dir = cfg.cert_dir
|
|
39
|
+
files.uniq.each do |name|
|
|
40
|
+
path = File.join(dir, name)
|
|
41
|
+
next unless File.exist?(path)
|
|
42
|
+
|
|
43
|
+
File.delete(path)
|
|
44
|
+
Ui.say(env, :info, "purge.removed", path: path)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Ui.say(env, :warn, "purge.failed", path: path, error: e.message)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def env_flag?(name)
|
|
51
|
+
ENV[name].to_s == "1"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Removes a configured certificate when the current machine is the last owner.
|
|
55
|
+
#
|
|
56
|
+
# Shared certificates stay installed until every machine recorded in the
|
|
57
|
+
# registry has released ownership.
|
|
58
|
+
#
|
|
59
|
+
# @param cfg [#cert_path, #cert_name] Vagrant certificate configuration.
|
|
60
|
+
# @param env [Hash, nil] Vagrant environment hash.
|
|
61
|
+
# @return [Hash] Normalized result with code, status, data, or error.
|
|
62
|
+
def self.perform_uninstall(cfg, env)
|
|
28
63
|
fp_entry = Registry.find_by_path(cfg.cert_path)
|
|
29
64
|
unless fp_entry
|
|
30
65
|
return({ code: 1, status: "error",
|
|
31
66
|
error: UiHelpers.t("errors.not_found_for_remove", path: cfg.cert_path) })
|
|
32
67
|
end
|
|
33
68
|
fp, rec = fp_entry
|
|
69
|
+
mid = env && env[:machine]&.id
|
|
70
|
+
|
|
71
|
+
if mid && Registry.release(fp, mid)
|
|
72
|
+
return({ code: 0, status: "success", data: { kept: true, cert: rec["name"] } })
|
|
73
|
+
end
|
|
74
|
+
|
|
34
75
|
os = OS.detect
|
|
35
76
|
ok = case os
|
|
36
77
|
when :mac then OS.mac_remove_by_fp(fp)
|
|
37
78
|
when :linux then OS.linux_uninstall_cert(rec["name"], nss: cfg.manage_nss_browsers,
|
|
38
79
|
firefox: cfg.manage_firefox)
|
|
39
|
-
when :windows then OS.win_remove_by_fp(fp)
|
|
80
|
+
when :windows then OS.win_remove_by_fp(fp, disable_firefox: !Registry.others_for_os?(fp, "windows"))
|
|
40
81
|
else return({ code: 2, status: "error", error: UiHelpers.t("errors.os_unsupported") })
|
|
41
82
|
end
|
|
42
83
|
Registry.untrack(fp) if ok
|
|
@@ -1,38 +1,86 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module VagrantDockerCertificatesManager
|
|
4
|
+
# Vagrant configuration for certificate generation, installation, and cleanup.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute cert_path
|
|
7
|
+
# @return [String] Path to the CA certificate installed into the trust store.
|
|
8
|
+
# @!attribute cert_name
|
|
9
|
+
# @return [String] Friendly certificate name used in host trust stores.
|
|
10
|
+
# @!attribute install_on_up
|
|
11
|
+
# @return [Boolean] Whether to install the certificate during `vagrant up`.
|
|
12
|
+
# @!attribute remove_on_destroy
|
|
13
|
+
# @return [Boolean] Whether to remove the certificate during `vagrant destroy`.
|
|
14
|
+
# @!attribute manage_firefox
|
|
15
|
+
# @return [Boolean] Whether Firefox stores should be managed where supported.
|
|
16
|
+
# @!attribute manage_nss_browsers
|
|
17
|
+
# @return [Boolean] Whether NSS browser stores should be managed where supported.
|
|
18
|
+
# @!attribute generate_on_up
|
|
19
|
+
# @return [Boolean] Whether local certificate material should be generated during `vagrant up`.
|
|
4
20
|
class Config < Vagrant.plugin("2", :config)
|
|
5
21
|
attr_accessor :cert_path, :cert_name, :install_on_up, :remove_on_destroy,
|
|
6
22
|
:manage_firefox, :manage_nss_browsers, :locale, :verbose,
|
|
7
|
-
:container_name
|
|
23
|
+
:container_name,
|
|
24
|
+
:generate_on_up, :ca_cn, :ca_days, :server_domain, :crl_url
|
|
8
25
|
|
|
9
26
|
def initialize
|
|
10
|
-
@cert_path =
|
|
11
|
-
@cert_name =
|
|
12
|
-
@install_on_up =
|
|
13
|
-
@remove_on_destroy =
|
|
14
|
-
@manage_firefox =
|
|
15
|
-
@manage_nss_browsers =
|
|
16
|
-
@locale =
|
|
17
|
-
@verbose =
|
|
18
|
-
@container_name =
|
|
27
|
+
@cert_path = UNSET_VALUE
|
|
28
|
+
@cert_name = UNSET_VALUE
|
|
29
|
+
@install_on_up = UNSET_VALUE
|
|
30
|
+
@remove_on_destroy = UNSET_VALUE
|
|
31
|
+
@manage_firefox = UNSET_VALUE
|
|
32
|
+
@manage_nss_browsers = UNSET_VALUE
|
|
33
|
+
@locale = UNSET_VALUE
|
|
34
|
+
@verbose = UNSET_VALUE
|
|
35
|
+
@container_name = UNSET_VALUE
|
|
36
|
+
@generate_on_up = UNSET_VALUE
|
|
37
|
+
@ca_cn = UNSET_VALUE
|
|
38
|
+
@ca_days = UNSET_VALUE
|
|
39
|
+
@server_domain = UNSET_VALUE
|
|
40
|
+
@crl_url = UNSET_VALUE
|
|
19
41
|
end
|
|
20
42
|
|
|
21
43
|
def finalize!
|
|
22
|
-
@cert_path
|
|
44
|
+
@cert_path = "certs/rootca.cert.pem" if @cert_path == UNSET_VALUE
|
|
45
|
+
@cert_name = "local.dev" if @cert_name == UNSET_VALUE
|
|
46
|
+
@install_on_up = false if @install_on_up == UNSET_VALUE
|
|
47
|
+
@remove_on_destroy = false if @remove_on_destroy == UNSET_VALUE
|
|
48
|
+
@manage_firefox = false if @manage_firefox == UNSET_VALUE
|
|
49
|
+
@manage_nss_browsers = true if @manage_nss_browsers == UNSET_VALUE
|
|
50
|
+
@locale = "en" if @locale == UNSET_VALUE
|
|
51
|
+
@verbose = false if @verbose == UNSET_VALUE
|
|
52
|
+
@container_name = nil if @container_name == UNSET_VALUE
|
|
53
|
+
@generate_on_up = false if @generate_on_up == UNSET_VALUE
|
|
54
|
+
@ca_cn = "local-ca" if @ca_cn == UNSET_VALUE
|
|
55
|
+
@ca_days = 3650 if @ca_days == UNSET_VALUE
|
|
56
|
+
@server_domain = nil if @server_domain == UNSET_VALUE
|
|
57
|
+
@crl_url = nil if @crl_url == UNSET_VALUE
|
|
58
|
+
|
|
23
59
|
@install_on_up = !!@install_on_up
|
|
24
60
|
@remove_on_destroy = !!@remove_on_destroy
|
|
25
61
|
@manage_firefox = !!@manage_firefox
|
|
26
62
|
@manage_nss_browsers = !!@manage_nss_browsers
|
|
27
63
|
@verbose = !!@verbose
|
|
64
|
+
@generate_on_up = !!@generate_on_up
|
|
28
65
|
@locale = (@locale || "en").to_s
|
|
66
|
+
@ca_cn = (@ca_cn || "local-ca").to_s
|
|
67
|
+
@ca_days = @ca_days.to_i
|
|
29
68
|
end
|
|
30
69
|
|
|
31
70
|
def validate(_machine)
|
|
32
71
|
errors = []
|
|
33
72
|
errors << "cert_path must be provided" if @cert_path.to_s.strip.empty?
|
|
34
73
|
errors << "cert_name must be provided" if @cert_name.to_s.strip.empty?
|
|
74
|
+
unless @locale.is_a?(String) && %w[en fr].include?(@locale.to_s[0, 2].downcase)
|
|
75
|
+
errors << "locale must be 'en' or 'fr'"
|
|
76
|
+
end
|
|
77
|
+
errors << "ca_days must be a positive integer" if @generate_on_up && @ca_days.to_i <= 0
|
|
35
78
|
{ "vagrant-docker-certificates-manager" => errors }
|
|
36
79
|
end
|
|
80
|
+
|
|
81
|
+
def cert_dir
|
|
82
|
+
d = File.dirname(@cert_path.to_s)
|
|
83
|
+
d.empty? ? "certs" : d
|
|
84
|
+
end
|
|
37
85
|
end
|
|
38
86
|
end
|
|
@@ -21,7 +21,8 @@ module VagrantDockerCertificatesManager
|
|
|
21
21
|
error: "❌",
|
|
22
22
|
version: "💾",
|
|
23
23
|
broom: "🧹",
|
|
24
|
-
question: "❓"
|
|
24
|
+
question: "❓",
|
|
25
|
+
bug: "🐛"
|
|
25
26
|
}.freeze
|
|
26
27
|
|
|
27
28
|
module_function
|
|
@@ -30,33 +31,34 @@ module VagrantDockerCertificatesManager
|
|
|
30
31
|
return if defined?(@i18n_setup) && @i18n_setup
|
|
31
32
|
::I18n.enforce_available_locales = false
|
|
32
33
|
base = File.expand_path("../../locales", __dir__)
|
|
33
|
-
|
|
34
|
+
paths = Dir[File.join(base, "*.yml")]
|
|
35
|
+
if paths.empty? && File.directory?(base)
|
|
36
|
+
paths = Dir.children(base).grep(/\.ya?ml\z/).map { |file| File.join(base, file) }
|
|
37
|
+
end
|
|
38
|
+
::I18n.load_path |= paths
|
|
34
39
|
::I18n.available_locales = SUPPORTED
|
|
35
|
-
default = ((ENV["VDCM_LANG"] || ENV["LANG"] || "en")[0,2] rescue "en").to_sym
|
|
40
|
+
default = ((ENV["VDCM_LANG"] || ENV["LANG"] || "en")[0, 2] rescue "en").to_sym
|
|
36
41
|
::I18n.default_locale = SUPPORTED.include?(default) ? default : :en
|
|
37
42
|
::I18n.backend.load_translations
|
|
38
43
|
@i18n_setup = true
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def set_locale!(lang, strict: false)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else
|
|
53
|
-
sym = :en
|
|
54
|
-
end
|
|
47
|
+
setup_i18n!
|
|
48
|
+
raw = (lang || ENV["VDCM_LANG"] || ENV["LANG"] || "en").to_s
|
|
49
|
+
sym = raw[0, 2].to_s.downcase.to_sym
|
|
50
|
+
sym = :en if sym.nil? || sym == :""
|
|
51
|
+
unless SUPPORTED.include?(sym)
|
|
52
|
+
if strict
|
|
53
|
+
raise UnsupportedLocaleError,
|
|
54
|
+
"#{e(:error)} Unsupported language: #{sym}. Available: #{SUPPORTED.join(', ')}"
|
|
55
|
+
else
|
|
56
|
+
sym = :en
|
|
55
57
|
end
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
end
|
|
59
|
+
::I18n.locale = sym
|
|
60
|
+
::I18n.backend.load_translations
|
|
61
|
+
sym
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def e(key, no_emoji: false)
|
|
@@ -88,6 +90,18 @@ module VagrantDockerCertificatesManager
|
|
|
88
90
|
v.is_a?(Hash) ? v : {}
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
def exists?(key)
|
|
94
|
+
::I18n.exists?(ns_key(key), ::I18n.locale)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def our_key?(k)
|
|
98
|
+
OUR_SPACES.any? { |ns| k.start_with?("#{NAMESPACE}.#{ns}") || k.start_with?(ns) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def debug_enabled?
|
|
102
|
+
ENV["VDCM_DEBUG"].to_s == "1"
|
|
103
|
+
end
|
|
104
|
+
|
|
91
105
|
def level_to_emoji(level)
|
|
92
106
|
case level
|
|
93
107
|
when :success then :success
|
|
@@ -114,8 +128,8 @@ module VagrantDockerCertificatesManager
|
|
|
114
128
|
end
|
|
115
129
|
|
|
116
130
|
def debug(env_or_ui, msg)
|
|
117
|
-
return unless
|
|
118
|
-
say(env_or_ui, :info, nil, raw: "#{e(:
|
|
131
|
+
return unless debug_enabled?
|
|
132
|
+
say(env_or_ui, :info, nil, raw: "#{e(:bug)} #{msg}")
|
|
119
133
|
end
|
|
120
134
|
|
|
121
135
|
def print_general_help(no_emoji: false, ui: nil)
|
|
@@ -125,6 +139,10 @@ module VagrantDockerCertificatesManager
|
|
|
125
139
|
lines << " #{t('cli.usage')}"
|
|
126
140
|
t_hash("help.commands").each_value { |line| lines << " #{line}" }
|
|
127
141
|
|
|
142
|
+
emit_lines(lines, ui)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def emit_lines(lines, ui)
|
|
128
146
|
if ui
|
|
129
147
|
lines.each { |ln| ui.info(ln) }
|
|
130
148
|
else
|
|
@@ -137,43 +155,53 @@ module VagrantDockerCertificatesManager
|
|
|
137
155
|
topic = (topic || "").to_s.strip.downcase
|
|
138
156
|
return print_general_help(no_emoji: no_emoji, ui: ui) if topic.empty?
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
usage = t("#{base}.usage", default: nil)
|
|
143
|
-
desc = t("#{base}.description", default: nil)
|
|
144
|
-
opts = t_hash("#{base}.options")
|
|
145
|
-
exs = ::I18n.t("#{base}.examples", default: [])
|
|
158
|
+
fields = topic_help_fields("help.topic.#{topic}")
|
|
159
|
+
return print_general_help(no_emoji: no_emoji, ui: ui) if topic_help_empty?(fields)
|
|
146
160
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
emit_lines(build_topic_help_lines(topic, fields, no_emoji), ui)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def topic_help_fields(base)
|
|
165
|
+
{
|
|
166
|
+
title: t("#{base}.title", default: nil),
|
|
167
|
+
usage: t("#{base}.usage", default: nil),
|
|
168
|
+
desc: t("#{base}.description", default: nil),
|
|
169
|
+
opts: t_hash("#{base}.options"),
|
|
170
|
+
exs: ::I18n.t(ns_key("#{base}.examples"), default: [])
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def topic_help_empty?(fields)
|
|
175
|
+
fields[:title].nil? && fields[:usage].nil? && fields[:desc].nil? &&
|
|
176
|
+
fields[:opts].empty? && fields[:exs].empty?
|
|
177
|
+
end
|
|
150
178
|
|
|
179
|
+
def build_topic_help_lines(topic, fields, no_emoji)
|
|
151
180
|
lines = []
|
|
152
|
-
lines << "#{e(:info, no_emoji: no_emoji)} #{title || t('help.topic_fallback_title', topic: topic)}"
|
|
181
|
+
lines << "#{e(:info, no_emoji: no_emoji)} #{fields[:title] || t('help.topic_fallback_title', topic: topic)}"
|
|
153
182
|
lines << " #{t('help.usage_label')}"
|
|
154
|
-
lines << " #{usage || 'vagrant certs help'}"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
end
|
|
163
|
-
if exs.is_a?(Array) && !exs.empty?
|
|
164
|
-
lines << " #{t('help.examples_label')}"
|
|
165
|
-
exs.each { |ex| lines << " #{ex}" }
|
|
183
|
+
lines << " #{fields[:usage] || 'vagrant certs help'}"
|
|
184
|
+
|
|
185
|
+
desc = fields[:desc]
|
|
186
|
+
append_help_section(lines, t("help.description_label"), [" #{desc}"]) if desc && !desc.strip.empty?
|
|
187
|
+
unless fields[:opts].empty?
|
|
188
|
+
append_help_section(lines, t("help.options_label"), fields[:opts].values.map do |l|
|
|
189
|
+
" #{l}"
|
|
190
|
+
end)
|
|
166
191
|
end
|
|
167
192
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
193
|
+
exs = fields[:exs]
|
|
194
|
+
if exs.is_a?(Array) && !exs.empty?
|
|
195
|
+
append_help_section(lines, t("help.examples_label"), exs.map do |ex|
|
|
196
|
+
" #{ex}"
|
|
197
|
+
end)
|
|
172
198
|
end
|
|
199
|
+
lines
|
|
173
200
|
end
|
|
174
201
|
|
|
175
|
-
def
|
|
176
|
-
|
|
202
|
+
def append_help_section(lines, label, body_lines)
|
|
203
|
+
lines << " #{label}"
|
|
204
|
+
body_lines.each { |line| lines << line }
|
|
177
205
|
end
|
|
178
206
|
end
|
|
179
207
|
end
|
|
@@ -31,5 +31,99 @@ module VagrantDockerCertificatesManager
|
|
|
31
31
|
def nickname_for(name)
|
|
32
32
|
"#{MARKER}:#{name}"
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
def fingerprint_of(cert)
|
|
36
|
+
OpenSSL::Digest::SHA1.hexdigest(cert.to_der).upcase
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generates a local development certificate authority.
|
|
40
|
+
#
|
|
41
|
+
# The generated CA is intended for local Docker/Vagrant workflows, not as a
|
|
42
|
+
# public browser-trusted certificate authority.
|
|
43
|
+
#
|
|
44
|
+
# @param cn [String] Common name for the CA certificate.
|
|
45
|
+
# @param days [Integer] Validity period in days.
|
|
46
|
+
# @param org [String] Organization attribute written to the subject.
|
|
47
|
+
# @param country [String] Country attribute written to the subject.
|
|
48
|
+
# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>] Generated certificate and private key.
|
|
49
|
+
def generate_ca(common_name:, days: 3650, org: "VDCM", country: "FR")
|
|
50
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
51
|
+
cert = OpenSSL::X509::Certificate.new
|
|
52
|
+
|
|
53
|
+
cert.version = 2
|
|
54
|
+
cert.serial = 1
|
|
55
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=#{common_name}/O=#{org}/C=#{country}")
|
|
56
|
+
cert.issuer = cert.subject
|
|
57
|
+
cert.public_key = key.public_key
|
|
58
|
+
cert.not_before = Time.now
|
|
59
|
+
cert.not_after = Time.now + (days * 24 * 60 * 60)
|
|
60
|
+
|
|
61
|
+
ef = OpenSSL::X509::ExtensionFactory.new(cert, cert)
|
|
62
|
+
cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
|
|
63
|
+
cert.add_extension(ef.create_extension("keyUsage", "keyCertSign,cRLSign", true))
|
|
64
|
+
cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash"))
|
|
65
|
+
cert.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
66
|
+
|
|
67
|
+
[cert, key]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def load_ca(cert_path, key_path)
|
|
71
|
+
[
|
|
72
|
+
OpenSSL::X509::Certificate.new(File.read(cert_path)),
|
|
73
|
+
OpenSSL::PKey::RSA.new(File.read(key_path))
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Generates a server certificate signed by the supplied local CA.
|
|
78
|
+
#
|
|
79
|
+
# Modern clients validate the SAN extension; the CN is kept mainly for
|
|
80
|
+
# readability and compatibility with older tooling.
|
|
81
|
+
#
|
|
82
|
+
# @param ca_cert [OpenSSL::X509::Certificate] CA certificate used as issuer.
|
|
83
|
+
# @param ca_key [OpenSSL::PKey::RSA] CA private key used to sign the certificate.
|
|
84
|
+
# @param domain [String] DNS name written to CN and subjectAltName.
|
|
85
|
+
# @param days [Integer] Validity period in days.
|
|
86
|
+
# @param crl_url [String, nil] Optional CRL distribution point URL.
|
|
87
|
+
# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>] Generated certificate and private key.
|
|
88
|
+
def generate_server(ca_cert, ca_key, domain:, days: 825, crl_url: nil)
|
|
89
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
90
|
+
cert = OpenSSL::X509::Certificate.new
|
|
91
|
+
|
|
92
|
+
cert.version = 2
|
|
93
|
+
cert.serial = 2
|
|
94
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=#{domain}")
|
|
95
|
+
cert.issuer = ca_cert.subject
|
|
96
|
+
cert.public_key = key.public_key
|
|
97
|
+
cert.not_before = Time.now
|
|
98
|
+
cert.not_after = Time.now + (days * 24 * 60 * 60)
|
|
99
|
+
|
|
100
|
+
ef = OpenSSL::X509::ExtensionFactory.new(ca_cert, cert)
|
|
101
|
+
cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{domain}"))
|
|
102
|
+
cert.add_extension(ef.create_extension("basicConstraints", "CA:FALSE"))
|
|
103
|
+
cert.add_extension(ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true))
|
|
104
|
+
cert.add_extension(ef.create_extension("extendedKeyUsage", "serverAuth"))
|
|
105
|
+
unless crl_url.to_s.strip.empty?
|
|
106
|
+
cert.add_extension(ef.create_extension("crlDistributionPoints", "URI:#{crl_url}"))
|
|
107
|
+
end
|
|
108
|
+
cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
|
|
109
|
+
|
|
110
|
+
[cert, key]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Generates an empty certificate revocation list for the local CA.
|
|
114
|
+
#
|
|
115
|
+
# @param ca_cert [OpenSSL::X509::Certificate] CA certificate used as issuer.
|
|
116
|
+
# @param ca_key [OpenSSL::PKey::RSA] CA private key used to sign the CRL.
|
|
117
|
+
# @param days [Integer] Validity period in days.
|
|
118
|
+
# @return [OpenSSL::X509::CRL] Signed CRL.
|
|
119
|
+
def generate_crl(ca_cert, ca_key, days: 3650)
|
|
120
|
+
crl = OpenSSL::X509::CRL.new
|
|
121
|
+
crl.issuer = ca_cert.subject
|
|
122
|
+
crl.version = 1
|
|
123
|
+
crl.last_update = Time.now
|
|
124
|
+
crl.next_update = Time.now + (days * 24 * 60 * 60)
|
|
125
|
+
crl.sign(ca_key, OpenSSL::Digest.new("SHA256"))
|
|
126
|
+
crl
|
|
127
|
+
end
|
|
34
128
|
end
|
|
35
129
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "base64"
|
|
4
5
|
require_relative "cert"
|
|
5
6
|
|
|
6
7
|
module VagrantDockerCertificatesManager
|
|
@@ -9,50 +10,57 @@ module VagrantDockerCertificatesManager
|
|
|
9
10
|
|
|
10
11
|
def detect
|
|
11
12
|
if defined?(Vagrant) && Vagrant.const_defined?(:Util) && Vagrant::Util.const_defined?(:Platform)
|
|
12
|
-
return :mac
|
|
13
|
+
return :mac if Vagrant::Util::Platform.darwin?
|
|
13
14
|
return :windows if Vagrant::Util::Platform.windows?
|
|
14
|
-
return :linux
|
|
15
|
+
return :linux if Vagrant::Util::Platform.linux?
|
|
15
16
|
else
|
|
16
17
|
plat = RbConfig::CONFIG["host_os"].downcase
|
|
17
|
-
return :mac
|
|
18
|
+
return :mac if plat.include?("darwin")
|
|
18
19
|
return :windows if plat =~ /mswin|mingw|windows/
|
|
19
|
-
return :linux
|
|
20
|
+
return :linux if plat.include?("linux")
|
|
20
21
|
end
|
|
21
22
|
:unknown
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# Runs an external command safely using the array form (no shell interpolation).
|
|
26
|
+
def run(*args)
|
|
27
|
+
out, err, st = Open3.capture3(*args)
|
|
26
28
|
[st.success?, out, err]
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
# ── macOS ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def mac_add_trusted_cert(path, _name)
|
|
34
|
+
run("sudo", "security", "add-trusted-cert",
|
|
35
|
+
"-d", "-r", "trustRoot",
|
|
36
|
+
"-k", "/Library/Keychains/System.keychain",
|
|
37
|
+
path.to_s).first
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def mac_has_cert_fingerprint?(fp)
|
|
35
|
-
ok, out,
|
|
36
|
-
|
|
37
|
-
out.include?(fp)
|
|
41
|
+
ok, out, = run("security", "find-certificate", "-a", "-Z",
|
|
42
|
+
"/Library/Keychains/System.keychain")
|
|
43
|
+
ok && out.include?(fp.to_s)
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def mac_remove_by_fp(fp)
|
|
41
|
-
ok, out,
|
|
47
|
+
ok, out, = run("security", "find-certificate", "-a", "-Z",
|
|
48
|
+
"/Library/Keychains/System.keychain")
|
|
42
49
|
return true unless ok
|
|
43
|
-
hash = out.lines.find { |l| l =~ /SHA-1 hash:\s*#{fp}/i } ? fp : nil
|
|
50
|
+
hash = out.lines.find { |l| l =~ /SHA-1 hash:\s*#{Regexp.escape(fp)}/i } ? fp : nil
|
|
44
51
|
return true unless hash
|
|
45
|
-
run(
|
|
52
|
+
run("sudo", "security", "delete-certificate",
|
|
53
|
+
"-Z", hash.to_s, "/Library/Keychains/System.keychain").first
|
|
46
54
|
end
|
|
47
55
|
|
|
56
|
+
# ── Linux ─────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
48
58
|
def linux_install_cert(path, name, nss: true, firefox: false)
|
|
49
59
|
dest = "/usr/local/share/ca-certificates/#{Cert::MARKER.downcase}-#{name}.crt"
|
|
50
|
-
ok1, = run(
|
|
51
|
-
ok2, = run("sudo update-ca-certificates")
|
|
52
|
-
okn = true
|
|
53
|
-
|
|
54
|
-
okf = true
|
|
55
|
-
okf &&= linux_firefox_install(path, name) if firefox
|
|
60
|
+
ok1, = run("sudo", "cp", path.to_s, dest)
|
|
61
|
+
ok2, = run("sudo", "update-ca-certificates")
|
|
62
|
+
okn = nss ? linux_nss_install(path, name) : true
|
|
63
|
+
okf = firefox ? linux_firefox_install(path, name) : true
|
|
56
64
|
ok1 && ok2 && okn && okf
|
|
57
65
|
end
|
|
58
66
|
|
|
@@ -62,71 +70,149 @@ module VagrantDockerCertificatesManager
|
|
|
62
70
|
|
|
63
71
|
def linux_uninstall_cert(name, nss: true, firefox: false)
|
|
64
72
|
dest = "/usr/local/share/ca-certificates/#{Cert::MARKER.downcase}-#{name}.crt"
|
|
65
|
-
run(
|
|
66
|
-
run("sudo update-ca-certificates")
|
|
67
|
-
linux_nss_uninstall(name)
|
|
73
|
+
run("sudo", "rm", "-f", dest)
|
|
74
|
+
run("sudo", "update-ca-certificates")
|
|
75
|
+
linux_nss_uninstall(name) if nss
|
|
68
76
|
linux_firefox_uninstall(name) if firefox
|
|
69
77
|
true
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
def linux_nss_install(path, name)
|
|
73
|
-
db =
|
|
74
|
-
run(
|
|
81
|
+
db = "sql:#{File.join(Dir.home, '.pki', 'nssdb')}"
|
|
82
|
+
run("certutil", "-d", db, "-A", "-t", "C,,",
|
|
83
|
+
"-n", Cert.nickname_for(name), "-i", path.to_s).first
|
|
75
84
|
end
|
|
76
85
|
|
|
77
86
|
def linux_nss_uninstall(name)
|
|
78
|
-
db =
|
|
79
|
-
run(
|
|
87
|
+
db = "sql:#{File.join(Dir.home, '.pki', 'nssdb')}"
|
|
88
|
+
run("certutil", "-d", db, "-D", "-n", Cert.nickname_for(name))
|
|
80
89
|
true
|
|
81
90
|
end
|
|
82
91
|
|
|
83
92
|
def linux_firefox_profiles
|
|
84
|
-
home =
|
|
93
|
+
home = Dir.home
|
|
85
94
|
[
|
|
86
95
|
"#{home}/.mozilla/firefox",
|
|
87
96
|
"#{home}/.var/app/org.mozilla.firefox/.mozilla/firefox",
|
|
88
97
|
"#{home}/snap/firefox/common/.mozilla/firefox"
|
|
89
|
-
].select { |d| File.directory?(d) }
|
|
98
|
+
].select { |d| File.directory?(d) }
|
|
99
|
+
.flat_map { |base| Dir.glob(File.join(base, "*.default*")) }
|
|
90
100
|
end
|
|
91
101
|
|
|
92
102
|
def linux_firefox_install(path, name)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
linux_firefox_profiles.all? do |profile|
|
|
104
|
+
run("certutil", "-A",
|
|
105
|
+
"-n", Cert.nickname_for(name), "-t", "C,,",
|
|
106
|
+
"-i", path.to_s, "-d", "sql:#{profile}").first
|
|
96
107
|
end
|
|
97
108
|
end
|
|
98
109
|
|
|
99
110
|
def linux_firefox_uninstall(name)
|
|
100
111
|
linux_firefox_profiles.each do |profile|
|
|
101
|
-
run(
|
|
112
|
+
run("certutil", "-D", "-n", Cert.nickname_for(name), "-d", "sql:#{profile}")
|
|
113
|
+
end
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# ── Windows ───────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
# Firefox on Windows does not ship with NSS certutil, so we enable
|
|
120
|
+
# security.enterprise_roots.enabled in each profile's user.js instead.
|
|
121
|
+
# This makes Firefox delegate trust to the Windows system cert store,
|
|
122
|
+
# which already contains the CA installed by win_install_cert.
|
|
123
|
+
|
|
124
|
+
FIREFOX_ENTERPRISE_ROOTS_PREF = 'user_pref("security.enterprise_roots.enabled", true);'
|
|
125
|
+
FIREFOX_ENTERPRISE_ROOTS_KEY = "security.enterprise_roots.enabled"
|
|
126
|
+
|
|
127
|
+
def win_firefox_profiles
|
|
128
|
+
appdata = (ENV["APPDATA"] || File.join(Dir.home, "AppData", "Roaming")).tr("\\", "/")
|
|
129
|
+
base = "#{appdata}/Mozilla/Firefox/Profiles"
|
|
130
|
+
return [] unless File.directory?(base)
|
|
131
|
+
Dir.glob("#{base}/*").select { |d| File.directory?(d) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def win_firefox_enable_enterprise_roots
|
|
135
|
+
win_firefox_profiles.each do |profile|
|
|
136
|
+
user_js = File.join(profile, "user.js")
|
|
137
|
+
content = File.exist?(user_js) ? File.read(user_js) : ""
|
|
138
|
+
next if content.include?(FIREFOX_ENTERPRISE_ROOTS_KEY)
|
|
139
|
+
File.open(user_js, "a") { |f| f.puts FIREFOX_ENTERPRISE_ROOTS_PREF }
|
|
140
|
+
end
|
|
141
|
+
true
|
|
142
|
+
rescue StandardError
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def win_firefox_disable_enterprise_roots
|
|
147
|
+
win_firefox_profiles.each do |profile|
|
|
148
|
+
user_js = File.join(profile, "user.js")
|
|
149
|
+
next unless File.exist?(user_js)
|
|
150
|
+
updated = File.read(user_js)
|
|
151
|
+
.lines
|
|
152
|
+
.reject { |l| l.include?(FIREFOX_ENTERPRISE_ROOTS_KEY) }
|
|
153
|
+
.join
|
|
154
|
+
File.write(user_js, updated)
|
|
102
155
|
end
|
|
103
156
|
true
|
|
157
|
+
rescue StandardError
|
|
158
|
+
false
|
|
104
159
|
end
|
|
105
160
|
|
|
106
161
|
def win_install_cert(path, name)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
162
|
+
fp = Cert.sha1(path)
|
|
163
|
+
nick = Cert.nickname_for(name).gsub("'", "''")
|
|
164
|
+
abs = File.expand_path(path).tr("/", "\\").gsub("'", "''")
|
|
165
|
+
|
|
166
|
+
ps = <<~PS
|
|
167
|
+
$ErrorActionPreference = 'Stop'
|
|
168
|
+
Import-Certificate -FilePath '#{abs}' -CertStoreLocation Cert:\\LocalMachine\\Root | Out-Null
|
|
169
|
+
$cert = Get-ChildItem Cert:\\LocalMachine\\Root | Where-Object { $_.Thumbprint -eq '#{fp}' }
|
|
170
|
+
if ($cert) { $cert.FriendlyName = '#{nick}' }
|
|
171
|
+
PS
|
|
172
|
+
encoded = Base64.strict_encode64(ps.encode("UTF-16LE"))
|
|
173
|
+
|
|
174
|
+
# Try non-elevated first (works if already admin)
|
|
175
|
+
ok, = run("powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded)
|
|
176
|
+
|
|
177
|
+
unless ok
|
|
178
|
+
# Elevate via UAC
|
|
179
|
+
elev = "Start-Process PowerShell -Verb RunAs -Wait " \
|
|
180
|
+
"-ArgumentList '-NonInteractive','-NoProfile','-EncodedCommand','#{encoded}'"
|
|
181
|
+
elev_encoded = Base64.strict_encode64(elev.encode("UTF-16LE"))
|
|
182
|
+
ok, = run("powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", elev_encoded)
|
|
183
|
+
return false unless ok
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
win_firefox_enable_enterprise_roots
|
|
115
187
|
true
|
|
116
188
|
end
|
|
117
189
|
|
|
118
190
|
def win_has_cert_fingerprint?(fp)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
ok, out, _ = run(%(powershell -NoProfile -NonInteractive -Command "#{ps}"))
|
|
125
|
-
ok && out.to_s.strip == "YES"
|
|
191
|
+
ps = "if (Get-ChildItem Cert:\\LocalMachine\\Root | " \
|
|
192
|
+
"Where-Object { $_.Thumbprint -eq '#{fp}' }) { 'YES' } else { 'NO' }"
|
|
193
|
+
ok, out, = run("powershell", "-NoProfile", "-NonInteractive",
|
|
194
|
+
"-EncodedCommand", Base64.strict_encode64(ps.encode("UTF-16LE")))
|
|
195
|
+
ok && out.to_s.strip == "YES"
|
|
126
196
|
end
|
|
127
197
|
|
|
128
198
|
def win_remove_by_fp(fp)
|
|
129
|
-
|
|
199
|
+
ps = <<~PS
|
|
200
|
+
$ErrorActionPreference = 'Stop'
|
|
201
|
+
Get-ChildItem Cert:\\LocalMachine\\Root |
|
|
202
|
+
Where-Object { $_.Thumbprint -eq '#{fp}' } |
|
|
203
|
+
Remove-Item
|
|
204
|
+
PS
|
|
205
|
+
encoded = Base64.strict_encode64(ps.encode("UTF-16LE"))
|
|
206
|
+
|
|
207
|
+
ok, = run("powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded)
|
|
208
|
+
return true if ok
|
|
209
|
+
|
|
210
|
+
elev = "Start-Process PowerShell -Verb RunAs -Wait " \
|
|
211
|
+
"-ArgumentList '-NonInteractive','-NoProfile','-EncodedCommand','#{encoded}'"
|
|
212
|
+
elev_encoded = Base64.strict_encode64(elev.encode("UTF-16LE"))
|
|
213
|
+
ok = run("powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", elev_encoded).first
|
|
214
|
+
win_firefox_disable_enterprise_roots if ok
|
|
215
|
+
ok
|
|
130
216
|
end
|
|
131
217
|
end
|
|
132
218
|
end
|
|
@@ -15,11 +15,17 @@ module VagrantDockerCertificatesManager
|
|
|
15
15
|
FileUtils.mkdir_p(File.dirname(db_path))
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Loads tracked certificates from the best-effort local registry.
|
|
19
|
+
#
|
|
20
|
+
# A corrupt or missing registry must not block Vagrant actions, because the
|
|
21
|
+
# OS trust store remains the source of truth.
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash] Registry data keyed by certificate fingerprint.
|
|
18
24
|
def load
|
|
19
25
|
ensure_dir!
|
|
20
26
|
return {} unless File.exist?(db_path)
|
|
21
27
|
JSON.parse(File.read(db_path))
|
|
22
|
-
rescue
|
|
28
|
+
rescue StandardError
|
|
23
29
|
{}
|
|
24
30
|
end
|
|
25
31
|
|
|
@@ -28,6 +34,11 @@ module VagrantDockerCertificatesManager
|
|
|
28
34
|
File.write(db_path, JSON.pretty_generate(data))
|
|
29
35
|
end
|
|
30
36
|
|
|
37
|
+
# Tracks a certificate fingerprint and its ownership metadata.
|
|
38
|
+
#
|
|
39
|
+
# @param fp [String] Certificate fingerprint.
|
|
40
|
+
# @param attrs [Hash] Persisted metadata such as path, name, OS, and owners.
|
|
41
|
+
# @return [void]
|
|
31
42
|
def track(fp, attrs)
|
|
32
43
|
data = load
|
|
33
44
|
data[fp] = attrs
|
|
@@ -41,6 +52,42 @@ module VagrantDockerCertificatesManager
|
|
|
41
52
|
removed
|
|
42
53
|
end
|
|
43
54
|
|
|
55
|
+
# Adds an owner to an already tracked certificate.
|
|
56
|
+
#
|
|
57
|
+
# @param fp [String] Certificate fingerprint.
|
|
58
|
+
# @param owner [String, #to_s] Vagrant machine id adopting the certificate.
|
|
59
|
+
# @return [Boolean] Whether the certificate was already tracked.
|
|
60
|
+
# rubocop:disable Naming/PredicateMethod -- boolean return kept for symmetry with #release
|
|
61
|
+
def adopt(fp, owner)
|
|
62
|
+
data = load
|
|
63
|
+
return false unless data[fp]
|
|
64
|
+
data[fp]["owners"] = Array(data[fp]["owners"]) | [owner.to_s]
|
|
65
|
+
save(data)
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
# rubocop:enable Naming/PredicateMethod
|
|
69
|
+
|
|
70
|
+
# Releases one owner from a tracked certificate.
|
|
71
|
+
#
|
|
72
|
+
# Keep the certificate installed while at least one Vagrant machine still
|
|
73
|
+
# owns it.
|
|
74
|
+
#
|
|
75
|
+
# @param fp [String] Certificate fingerprint.
|
|
76
|
+
# @param owner [String, #to_s] Vagrant machine id releasing the certificate.
|
|
77
|
+
# @return [Boolean] Whether other owners remain after the release.
|
|
78
|
+
# rubocop:disable Naming/PredicateMethod -- boolean return kept for symmetry with #adopt
|
|
79
|
+
def release(fp, owner)
|
|
80
|
+
data = load
|
|
81
|
+
rec = data[fp]
|
|
82
|
+
return false unless rec
|
|
83
|
+
owners = Array(rec["owners"])
|
|
84
|
+
owners.delete(owner.to_s)
|
|
85
|
+
rec["owners"] = owners
|
|
86
|
+
save(data)
|
|
87
|
+
!owners.empty?
|
|
88
|
+
end
|
|
89
|
+
# rubocop:enable Naming/PredicateMethod
|
|
90
|
+
|
|
44
91
|
def find_by_path(path)
|
|
45
92
|
data = load
|
|
46
93
|
data.find { |_fp, v| File.expand_path(v["path"]) == File.expand_path(path) }
|
|
@@ -49,5 +96,9 @@ module VagrantDockerCertificatesManager
|
|
|
49
96
|
def all
|
|
50
97
|
load
|
|
51
98
|
end
|
|
99
|
+
|
|
100
|
+
def others_for_os?(fp, os)
|
|
101
|
+
load.any? { |k, v| k != fp && v["os"].to_s == os.to_s }
|
|
102
|
+
end
|
|
52
103
|
end
|
|
53
104
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module VagrantDockerCertificatesManager
|
|
4
|
-
VERSION
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
unless defined?(VERSION)
|
|
5
|
+
VERSION = begin
|
|
6
|
+
path = File.expand_path("VERSION", __dir__)
|
|
7
|
+
File.exist?(path) ? File.read(path).strip : "0.1.0"
|
|
8
|
+
rescue StandardError
|
|
9
|
+
"0.1.0"
|
|
10
|
+
end
|
|
9
11
|
end
|
|
10
12
|
end
|
data/locales/en.yml
CHANGED
|
@@ -80,14 +80,3 @@ en:
|
|
|
80
80
|
success: "Certificate %{name} removed."
|
|
81
81
|
fail: "Failed to remove certificate %{name}."
|
|
82
82
|
skip: "Remove on destroy disabled; skipping."
|
|
83
|
-
|
|
84
|
-
errors:
|
|
85
|
-
invalid_path: "Invalid certificate path: %{path}"
|
|
86
|
-
missing_path_remove: "You must provide a path for removal."
|
|
87
|
-
not_found_for_remove: "No tracked certificate found for path: %{path}"
|
|
88
|
-
already_present: "The certificate %{name} already exists."
|
|
89
|
-
install_failed: "Certificate installation failed."
|
|
90
|
-
uninstall_failed: "Certificate removal failed."
|
|
91
|
-
remove_failed: "Remove failed."
|
|
92
|
-
os_unsupported: "Unsupported OS for this action."
|
|
93
|
-
unknown_command: "Unknown command: %{cmd}"
|
data/locales/fr.yml
CHANGED
|
@@ -80,14 +80,3 @@ fr:
|
|
|
80
80
|
success: "Certificat %{name} supprimé."
|
|
81
81
|
fail: "Échec de la suppression du certificat %{name}."
|
|
82
82
|
skip: "Suppression à la destruction désactivée ; on ignore."
|
|
83
|
-
|
|
84
|
-
errors:
|
|
85
|
-
invalid_path: "Chemin de certificat invalide : %{path}"
|
|
86
|
-
missing_path_remove: "Vous devez fournir un chemin à supprimer."
|
|
87
|
-
not_found_for_remove: "Aucun certificat suivi pour le chemin : %{path}"
|
|
88
|
-
already_present: "Le certificat %{name} existe déjà."
|
|
89
|
-
install_failed: "Échec de l'installation du certificat."
|
|
90
|
-
uninstall_failed: "Échec de la suppression du certificat."
|
|
91
|
-
remove_failed: "Échec de la suppression."
|
|
92
|
-
os_unsupported: "Système non pris en charge pour cette action."
|
|
93
|
-
unknown_command: "Commande inconnue : %{cmd}"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vagrant-docker-certificates-manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julien Poirou
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-07-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: i18n
|