openvoxserver-ca 3.0.0.pre.rc1

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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +17 -0
  3. data/.github/release.yml +41 -0
  4. data/.github/workflows/gem_release.yaml +106 -0
  5. data/.github/workflows/prepare_release.yml +28 -0
  6. data/.github/workflows/release.yml +28 -0
  7. data/.github/workflows/unit_tests.yaml +45 -0
  8. data/.gitignore +14 -0
  9. data/.rspec +2 -0
  10. data/.travis.yml +16 -0
  11. data/CHANGELOG.md +15 -0
  12. data/CODEOWNERS +4 -0
  13. data/CODE_OF_CONDUCT.md +74 -0
  14. data/CONTRIBUTING.md +15 -0
  15. data/Gemfile +20 -0
  16. data/LICENSE +202 -0
  17. data/README.md +118 -0
  18. data/Rakefile +30 -0
  19. data/bin/console +14 -0
  20. data/bin/setup +8 -0
  21. data/exe/puppetserver-ca +10 -0
  22. data/lib/puppetserver/ca/action/clean.rb +109 -0
  23. data/lib/puppetserver/ca/action/delete.rb +286 -0
  24. data/lib/puppetserver/ca/action/enable.rb +140 -0
  25. data/lib/puppetserver/ca/action/generate.rb +330 -0
  26. data/lib/puppetserver/ca/action/import.rb +196 -0
  27. data/lib/puppetserver/ca/action/list.rb +253 -0
  28. data/lib/puppetserver/ca/action/migrate.rb +97 -0
  29. data/lib/puppetserver/ca/action/prune.rb +289 -0
  30. data/lib/puppetserver/ca/action/revoke.rb +108 -0
  31. data/lib/puppetserver/ca/action/setup.rb +188 -0
  32. data/lib/puppetserver/ca/action/sign.rb +146 -0
  33. data/lib/puppetserver/ca/certificate_authority.rb +418 -0
  34. data/lib/puppetserver/ca/cli.rb +145 -0
  35. data/lib/puppetserver/ca/config/puppet.rb +309 -0
  36. data/lib/puppetserver/ca/config/puppetserver.rb +84 -0
  37. data/lib/puppetserver/ca/errors.rb +40 -0
  38. data/lib/puppetserver/ca/host.rb +176 -0
  39. data/lib/puppetserver/ca/local_certificate_authority.rb +304 -0
  40. data/lib/puppetserver/ca/logger.rb +49 -0
  41. data/lib/puppetserver/ca/stub.rb +17 -0
  42. data/lib/puppetserver/ca/utils/cli_parsing.rb +67 -0
  43. data/lib/puppetserver/ca/utils/config.rb +61 -0
  44. data/lib/puppetserver/ca/utils/file_system.rb +109 -0
  45. data/lib/puppetserver/ca/utils/http_client.rb +232 -0
  46. data/lib/puppetserver/ca/utils/inventory.rb +84 -0
  47. data/lib/puppetserver/ca/utils/signing_digest.rb +27 -0
  48. data/lib/puppetserver/ca/version.rb +5 -0
  49. data/lib/puppetserver/ca/x509_loader.rb +170 -0
  50. data/lib/puppetserver/ca.rb +7 -0
  51. data/openvoxserver-ca.gemspec +31 -0
  52. data/tasks/spec.rake +15 -0
  53. data/tasks/vox.rake +19 -0
  54. metadata +154 -0
@@ -0,0 +1,304 @@
1
+ require 'openssl'
2
+
3
+ require 'puppetserver/ca/host'
4
+ require 'puppetserver/ca/utils/file_system'
5
+ require 'puppetserver/ca/x509_loader'
6
+
7
+ module Puppetserver
8
+ module Ca
9
+ class LocalCertificateAuthority
10
+
11
+ # Make the certificate valid as of yesterday, because so many people's
12
+ # clocks are out of sync. This gives one more day of validity than people
13
+ # might expect, but is better than making every person who has a messed up
14
+ # clock fail, and better than having every cert we generate expire a day
15
+ # before the user expected it to when they asked for "one year".
16
+ CERT_VALID_FROM = (Time.now - (60*60*24)).freeze
17
+
18
+ SSL_SERVER_CERT = "serverAuth"
19
+ SSL_CLIENT_CERT = "clientAuth"
20
+
21
+ CLI_AUTH_EXT_OID = "1.3.6.1.4.1.34380.1.3.39"
22
+
23
+ SERVER_EXTENSIONS = [
24
+ ["basicConstraints", "CA:FALSE", true],
25
+ ["nsComment", "Puppet Server Internal Certificate", false],
26
+ ["authorityKeyIdentifier", "keyid:always", false],
27
+ ["extendedKeyUsage", "#{SSL_SERVER_CERT}, #{SSL_CLIENT_CERT}", true],
28
+ ["keyUsage", "keyEncipherment, digitalSignature", true],
29
+ ["subjectKeyIdentifier", "hash", false]
30
+ ].freeze
31
+
32
+ CA_EXTENSIONS = [
33
+ ["basicConstraints", "CA:TRUE", true],
34
+ ["keyUsage", "keyCertSign, cRLSign", true],
35
+ ["subjectKeyIdentifier", "hash", false],
36
+ ["nsComment", "Puppet Server Internal Certificate", false],
37
+ ["authorityKeyIdentifier", "keyid:always", false]
38
+ ].freeze
39
+
40
+ attr_reader :cert, :cert_bundle, :key, :crl, :crl_chain
41
+
42
+ def initialize(digest, settings)
43
+ @digest = digest
44
+ @host = Host.new(digest)
45
+ @settings = settings
46
+ @errors = []
47
+
48
+ if ssl_assets_exist?
49
+ loader = Puppetserver::Ca::X509Loader.new(@settings[:cacert], @settings[:cakey], @settings[:cacrl])
50
+ if loader.errors.empty?
51
+ load_ssl_components(loader)
52
+ else
53
+ @errors += loader.errors
54
+ @errors << "CA not initialized. Please set up your CA before attempting to generate certs offline."
55
+ end
56
+ end
57
+ end
58
+
59
+ def ssl_assets_exist?
60
+ File.exist?(@settings[:cacert]) &&
61
+ File.exist?(@settings[:cakey]) &&
62
+ File.exist?(@settings[:cacrl])
63
+ end
64
+
65
+ def load_ssl_components(loader)
66
+ @cert_bundle = loader.certs
67
+ @key = loader.key
68
+ @cert = loader.cert
69
+ @crl_chain = loader.crls
70
+ @crl = loader.crl
71
+ end
72
+
73
+ # Initialize SSL state
74
+ #
75
+ # This method is similar to {#load_ssl_components}, but has extra
76
+ # logic for initializing components that may not be present when
77
+ # the CA is set up for the first time. For example, SSL components
78
+ # provided by an external CA will often not include a pre-generated
79
+ # leaf CRL.
80
+ #
81
+ # @note Check {#errors} after calling this method for issues that
82
+ # may have occurred during initialization.
83
+ #
84
+ # @param loader [Puppetserver::Ca::X509Loader]
85
+ # @return [void]
86
+ def initialize_ssl_components(loader)
87
+ @cert_bundle = loader.certs
88
+ @key = loader.key
89
+ @cert = loader.cert
90
+
91
+ if loader.crl.nil?
92
+ loader.crl = create_crl_for(@cert, @key)
93
+
94
+ loader.validate_full_chain(@cert_bundle, loader.crls)
95
+ @errors += loader.errors
96
+ end
97
+
98
+ @crl_chain = loader.crls
99
+ @crl = loader.crl
100
+ end
101
+
102
+ def errors
103
+ @errors += @host.errors
104
+ end
105
+
106
+ def valid_until
107
+ Time.now + @settings[:ca_ttl]
108
+ end
109
+
110
+ def extension_factory_for(ca, cert = nil)
111
+ ef = OpenSSL::X509::ExtensionFactory.new
112
+ ef.issuer_certificate = ca
113
+ ef.subject_certificate = cert if cert
114
+
115
+ ef
116
+ end
117
+
118
+ def inventory_entry(cert)
119
+ "0x%04x %s %s %s" % [cert.serial, format_time(cert.not_before),
120
+ format_time(cert.not_after), cert.subject]
121
+ end
122
+
123
+ def next_serial(serial_file)
124
+ if File.exist?(serial_file)
125
+ File.read(serial_file).to_i(16)
126
+ else
127
+ 1
128
+ end
129
+ end
130
+
131
+ def format_time(time)
132
+ time.strftime('%Y-%m-%dT%H:%M:%S%Z')
133
+ end
134
+
135
+ def create_server_cert
136
+ server_cert = nil
137
+ server_key = @host.create_private_key(@settings[:keylength],
138
+ @settings[:hostprivkey],
139
+ @settings[:hostpubkey])
140
+ if server_key
141
+ server_csr = @host.create_csr(name: @settings[:certname], key: server_key)
142
+ if @settings[:subject_alt_names].empty?
143
+ alt_names = "DNS:puppet, DNS:#{@settings[:certname]}"
144
+ else
145
+ alt_names = @settings[:subject_alt_names]
146
+ end
147
+
148
+ server_cert = sign_authorized_cert(server_csr, alt_names)
149
+ end
150
+
151
+ return server_key, server_cert
152
+ end
153
+
154
+ def sign_authorized_cert(csr, alt_names = '')
155
+ cert = OpenSSL::X509::Certificate.new
156
+ cert.public_key = csr.public_key
157
+ cert.subject = csr.subject
158
+ cert.issuer = @cert.subject
159
+ cert.version = 2
160
+ cert.serial = next_serial(@settings[:serial])
161
+ cert.not_before = CERT_VALID_FROM
162
+ cert.not_after = valid_until
163
+
164
+ return unless add_custom_extensions(cert)
165
+
166
+ ef = extension_factory_for(@cert, cert)
167
+ add_authorized_extensions(cert, ef)
168
+
169
+ if !alt_names.empty?
170
+ add_subject_alt_names_extension(alt_names, cert, ef)
171
+ end
172
+
173
+ cert.sign(@key, @digest)
174
+
175
+ cert
176
+ end
177
+
178
+ def add_authorized_extensions(cert, ef)
179
+ SERVER_EXTENSIONS.each do |ext|
180
+ extension = ef.create_extension(*ext)
181
+ cert.add_extension(extension)
182
+ end
183
+
184
+ # Status API access for the CA CLI
185
+ cli_auth_ext = OpenSSL::X509::Extension.new(CLI_AUTH_EXT_OID, OpenSSL::ASN1::UTF8String.new("true").to_der, false)
186
+ cert.add_extension(cli_auth_ext)
187
+ end
188
+
189
+ def add_subject_alt_names_extension(alt_names, cert, ef)
190
+ alt_names_ext = ef.create_extension("subjectAltName", alt_names, false)
191
+ cert.add_extension(alt_names_ext)
192
+ end
193
+
194
+ # This takes all the extension requests from csr_attributes.yaml and
195
+ # adds those to the cert
196
+ def add_custom_extensions(cert)
197
+ extension_requests = @host.get_extension_requests(@settings[:csr_attributes])
198
+
199
+ if extension_requests
200
+ extensions = @host.validated_extensions(extension_requests)
201
+ extensions.each do |ext|
202
+ cert.add_extension(ext)
203
+ end
204
+ end
205
+
206
+ @host.errors.empty?
207
+ end
208
+
209
+ def create_root_cert
210
+ root_key = @host.create_private_key(@settings[:keylength])
211
+ root_cert = self_signed_ca(root_key)
212
+ root_crl = create_crl_for(root_cert, root_key)
213
+
214
+ return root_key, root_cert, root_crl
215
+ end
216
+
217
+ def self_signed_ca(key)
218
+ cert = OpenSSL::X509::Certificate.new
219
+
220
+ cert.public_key = key.public_key
221
+ cert.subject = OpenSSL::X509::Name.new([["CN", @settings[:root_ca_name]]])
222
+ cert.issuer = cert.subject
223
+ cert.version = 2
224
+ cert.serial = 1
225
+
226
+ cert.not_before = CERT_VALID_FROM
227
+ cert.not_after = valid_until
228
+
229
+ ef = extension_factory_for(cert, cert)
230
+ CA_EXTENSIONS.each do |ext|
231
+ extension = ef.create_extension(*ext)
232
+ cert.add_extension(extension)
233
+ end
234
+
235
+ cert.sign(key, @digest)
236
+
237
+ cert
238
+ end
239
+
240
+ def create_crl_for(cert, key)
241
+ crl = OpenSSL::X509::CRL.new
242
+ crl.version = 1
243
+ crl.issuer = cert.subject
244
+
245
+ ef = extension_factory_for(cert)
246
+ crl.add_extension(
247
+ ef.create_extension(["authorityKeyIdentifier", "keyid:always", false]))
248
+ crl.add_extension(
249
+ OpenSSL::X509::Extension.new("crlNumber", OpenSSL::ASN1::Integer(0)))
250
+
251
+ crl.last_update = CERT_VALID_FROM
252
+ crl.next_update = valid_until
253
+ crl.sign(key, @digest)
254
+
255
+ # FIXME: Workaround a bug in jruby-openssl. Without this, #to_pem return an invalid CRL:
256
+ # ----BEGIN X509 CRL-----
257
+ # MAA=
258
+ # -----END X509 CRL-----
259
+ # See:
260
+ # https://github.com/jruby/jruby-openssl/issues/163
261
+ # https://github.com/jruby/jruby-openssl/pull/333
262
+ crl = OpenSSL::X509::CRL.new(crl.to_der)
263
+
264
+ crl
265
+ end
266
+
267
+ def create_intermediate_cert(root_key, root_cert)
268
+ @key = @host.create_private_key(@settings[:keylength])
269
+ int_csr = @host.create_csr(name: @settings[:ca_name], key: @key)
270
+ @cert = sign_intermediate(root_key, root_cert, int_csr)
271
+ @crl = create_crl_for(@cert, @key)
272
+
273
+ return nil
274
+ end
275
+
276
+ def sign_intermediate(ca_key, ca_cert, csr)
277
+ cert = OpenSSL::X509::Certificate.new
278
+
279
+ cert.public_key = csr.public_key
280
+ cert.subject = csr.subject
281
+ cert.issuer = ca_cert.subject
282
+ cert.version = 2
283
+ cert.serial = 2
284
+
285
+ cert.not_before = CERT_VALID_FROM
286
+ cert.not_after = valid_until
287
+
288
+ ef = extension_factory_for(ca_cert, cert)
289
+ CA_EXTENSIONS.each do |ext|
290
+ extension = ef.create_extension(*ext)
291
+ cert.add_extension(extension)
292
+ end
293
+
294
+ cert.sign(ca_key, @digest)
295
+
296
+ cert
297
+ end
298
+
299
+ def update_serial_file(serial)
300
+ Puppetserver::Ca::Utils::FileSystem.write_file(@settings[:serial], serial.to_s(16), 0644)
301
+ end
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,49 @@
1
+ module Puppetserver
2
+ module Ca
3
+ class Logger
4
+ LEVELS = {error: 1, warning: 2, info: 3, debug: 4}
5
+
6
+ def initialize(level = :info, out = STDOUT, err = STDERR)
7
+ @level = LEVELS[level]
8
+ if @level.nil?
9
+ raise ArgumentError, "Unknown log level #{level}"
10
+ end
11
+
12
+ @out = out
13
+ @err = err
14
+ end
15
+
16
+ def level
17
+ @level
18
+ end
19
+
20
+ def debug?
21
+ return @level >= LEVELS[:debug]
22
+ end
23
+
24
+ def debug(text)
25
+ if debug?
26
+ @out.puts(text)
27
+ end
28
+ end
29
+
30
+ def inform(text)
31
+ if @level >= LEVELS[:info]
32
+ @out.puts(text)
33
+ end
34
+ end
35
+
36
+ def warn(text)
37
+ if @level >= LEVELS[:warning]
38
+ @err.puts(text)
39
+ end
40
+ end
41
+
42
+ def err(text)
43
+ if @level >= LEVELS[:error]
44
+ @err.puts(text)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+
3
+ module Puppetserver
4
+ module Ca
5
+ module Stub
6
+ KEY_PATH = '/etc/puppetlabs/puppet/ssl/ca/ca_key.pem'
7
+ BUNDLE_PATH = '/etc/puppetlabs/puppet/ssl/ca/ca_crt.pem'
8
+ CRL_PATH = '/etc/puppetlabs/puppet/ssl/ca/ca_crl.pem'
9
+
10
+ def self.import(key, bundle, crl)
11
+ FileUtils.copy(File.absolute_path(key), KEY_PATH)
12
+ FileUtils.copy(File.absolute_path(bundle), BUNDLE_PATH)
13
+ FileUtils.copy(File.absolute_path(crl), CRL_PATH)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ module Puppetserver
2
+ module Ca
3
+ module Utils
4
+ module CliParsing
5
+ def self.parse_without_raising(parser, args)
6
+ all, not_flags, malformed_flags, unknown_flags = [], [], [], []
7
+
8
+ begin
9
+ # OptionParser calls this block when it finds a value that doesn't
10
+ # start with one or two dashes and doesn't follow a flag that
11
+ # consumes a value.
12
+ parser.order!(args) do |not_flag|
13
+ not_flags << not_flag
14
+ all << not_flag
15
+ end
16
+ rescue OptionParser::MissingArgument => e
17
+ malformed_flags += e.args
18
+ all += e.args
19
+
20
+ retry
21
+ rescue OptionParser::ParseError => e
22
+ flag = e.args.first
23
+ unknown_flags << flag
24
+ all << flag
25
+
26
+ if does_not_contain_argument(flag) &&
27
+ args.first &&
28
+ next_arg_is_not_another_flag(args.first)
29
+
30
+ value = args.shift
31
+ unknown_flags << value
32
+ all << value
33
+ end
34
+
35
+ retry
36
+ end
37
+
38
+ return all, not_flags, malformed_flags, unknown_flags
39
+ end
40
+
41
+ def self.parse_with_errors(parser, args)
42
+ errors = []
43
+
44
+ _, non_flags, malformed_flags, unknown_flags = parse_without_raising(parser, args)
45
+
46
+ malformed_flags.each {|f| errors << " Missing argument to flag `#{f}`" }
47
+ unknown_flags.each {|f| errors << " Unknown flag or argument `#{f}`" }
48
+ non_flags.each {|f| errors << " Unknown input `#{f}`" }
49
+
50
+ errors
51
+ end
52
+
53
+
54
+ private
55
+
56
+ # eg. --flag=argument-to-flag
57
+ def self.does_not_contain_argument(flag)
58
+ !flag.include?('=')
59
+ end
60
+
61
+ def self.next_arg_is_not_another_flag(maybe_an_arg)
62
+ !maybe_an_arg.start_with?('-')
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,61 @@
1
+ require 'puppetserver/ca/utils/file_system'
2
+
3
+ module Puppetserver
4
+ module Ca
5
+ module Utils
6
+ module Config
7
+
8
+ def self.running_as_root?
9
+ !Gem.win_platform? && Process::UID.eid == 0
10
+ end
11
+
12
+ def self.munge_alt_names(names)
13
+ raw_names = names.split(/\s*,\s*/).map(&:strip)
14
+ munged_names = raw_names.map do |name|
15
+ # Prepend the DNS tag if no tag was specified
16
+ if !name.start_with?("IP:") && !name.start_with?("DNS:")
17
+ "DNS:#{name}"
18
+ else
19
+ name
20
+ end
21
+ end.sort.uniq.join(", ")
22
+ end
23
+
24
+ def self.puppet_confdir
25
+ if running_as_root?
26
+ '/etc/puppetlabs/puppet'
27
+ else
28
+ "#{ENV['HOME']}/.puppetlabs/etc/puppet"
29
+ end
30
+ end
31
+
32
+ def self.puppetserver_confdir(puppet_confdir)
33
+ File.join(File.dirname(puppet_confdir), 'puppetserver')
34
+ end
35
+
36
+ def self.default_ssldir(confdir = puppet_confdir)
37
+ File.join(confdir, 'ssl')
38
+ end
39
+
40
+ def self.old_default_cadir(confdir = puppet_confdir)
41
+ File.join(confdir, 'ssl', 'ca')
42
+ end
43
+
44
+ def self.new_default_cadir(confdir = puppet_confdir)
45
+ File.join(puppetserver_confdir(confdir), 'ca')
46
+ end
47
+
48
+ def self.symlink_to_old_cadir(current_cadir, puppet_confdir)
49
+ old_cadir = old_default_cadir(puppet_confdir)
50
+ new_cadir = new_default_cadir(puppet_confdir)
51
+ return if current_cadir != new_cadir
52
+ # This is only run on setup/import, so there should be no files in the
53
+ # old cadir, so it should be safe to forcibly remove it (which we need
54
+ # to do in order to create a symlink).
55
+ Puppetserver::Ca::Utils::FileSystem.forcibly_symlink(new_cadir, old_cadir)
56
+ end
57
+
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,109 @@
1
+ require 'etc'
2
+ require 'fileutils'
3
+
4
+ module Puppetserver
5
+ module Ca
6
+ module Utils
7
+ class FileSystem
8
+
9
+ DIR_MODES = {
10
+ :ssldir => 0771,
11
+ :cadir => 0755,
12
+ :certdir => 0755,
13
+ :privatekeydir => 0750,
14
+ :publickeydir => 0755,
15
+ :signeddir => 0755
16
+ }
17
+
18
+ def self.instance
19
+ @instance ||= new
20
+ end
21
+
22
+ def self.write_file(*args)
23
+ instance.write_file(*args)
24
+ end
25
+
26
+ def self.ensure_dirs(one_or_more_dirs)
27
+ Array(one_or_more_dirs).each do |directory|
28
+ instance.ensure_dir(directory)
29
+ end
30
+ end
31
+
32
+ def self.validate_file_paths(one_or_more_paths)
33
+ errors = []
34
+ Array(one_or_more_paths).each do |path|
35
+ if !File.exist?(path) || !File.readable?(path)
36
+ errors << "Could not read file '#{path}'"
37
+ end
38
+ end
39
+
40
+ errors
41
+ end
42
+
43
+ def self.check_for_existing_files(one_or_more_paths)
44
+ errors = []
45
+ Array(one_or_more_paths).each do |path|
46
+ if File.exist?(path)
47
+ errors << "Existing file at '#{path}'"
48
+ end
49
+ end
50
+ errors
51
+ end
52
+
53
+ def self.forcibly_symlink(source, link_target)
54
+ FileUtils.remove_dir(link_target, true)
55
+ FileUtils.symlink(source, link_target)
56
+ # Ensure the symlink has the same ownership as the source.
57
+ # This requires using `FileUtils.chown` rather than `File.chown`, as
58
+ # the latter will update the ownership of the source rather than the
59
+ # link itself.
60
+ # Symlink permissions are ignored in favor of the source's permissions,
61
+ # so we don't have to change those.
62
+ source_info = File.stat(source)
63
+ FileUtils.chown(source_info.uid, source_info.gid, link_target)
64
+ end
65
+
66
+ def initialize
67
+ @user, @group = find_user_and_group
68
+ end
69
+
70
+ def find_user_and_group
71
+ if !running_as_root?
72
+ return Process.euid, Process.egid
73
+ else
74
+ if pe_puppet_exists?
75
+ return 'pe-puppet', 'pe-puppet'
76
+ else
77
+ return 'puppet', 'puppet'
78
+ end
79
+ end
80
+ end
81
+
82
+ def running_as_root?
83
+ !Gem.win_platform? && Process.euid == 0
84
+ end
85
+
86
+ def pe_puppet_exists?
87
+ !!(Etc.getpwnam('pe-puppet') rescue nil)
88
+ end
89
+
90
+ def write_file(path, one_or_more_objects, mode)
91
+ File.open(path, 'w', mode) do |f|
92
+ Array(one_or_more_objects).each do |object|
93
+ f.puts object.to_s
94
+ end
95
+ end
96
+ FileUtils.chown(@user, @group, path)
97
+ end
98
+
99
+ # Warning: directory mode should be specified in DIR_MODES above
100
+ def ensure_dir(directory)
101
+ if !File.exist?(directory)
102
+ FileUtils.mkdir_p(directory, mode: DIR_MODES[directory])
103
+ FileUtils.chown(@user, @group, directory)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end