summaryrefslogtreecommitdiff
path: root/spec/unit/indirector/ssl_file_spec.rb
blob: 8d13bdc948127d7e64aeaf0f331f0f396c0e1198 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#! /usr/bin/env ruby
require 'spec_helper'

require 'puppet/indirector/ssl_file'

describe Puppet::Indirector::SslFile do
  include PuppetSpec::Files

  before :all do
    @indirection = stub 'indirection', :name => :testing, :model => @model
    Puppet::Indirector::Indirection.expects(:instance).with(:testing).returns(@indirection)
    module Testing; end
    @file_class = class Testing::MyType < Puppet::Indirector::SslFile
      self
    end
  end
  before :each do
    @model = mock 'model'

    @setting = :certdir
    @file_class.store_in @setting
    @file_class.store_at nil
    @file_class.store_ca_at nil
    @path = make_absolute("/thisdoesntexist/my_directory")
    Puppet[:noop] = false
    Puppet[@setting] = @path
    Puppet[:trace] = false
  end

  after :each do
    @file_class.store_in nil
    @file_class.store_at nil
    @file_class.store_ca_at nil
  end

  it "should use :main and :ssl upon initialization" do
    Puppet.settings.expects(:use).with(:main, :ssl)
    @file_class.new
  end

  it "should return a nil collection directory if no directory setting has been provided" do
    @file_class.store_in nil
    @file_class.collection_directory.should be_nil
  end

  it "should return a nil file location if no location has been provided" do
    @file_class.store_at nil
    @file_class.file_location.should be_nil
  end

  it "should fail if no store directory or file location has been set" do
    Puppet.settings.expects(:use).with(:main, :ssl)
    @file_class.store_in nil
    @file_class.store_at nil
    expect {
      @file_class.new
    }.to raise_error(Puppet::DevError, /No file or directory setting provided/)
  end

  describe "when managing ssl files" do
    before do
      Puppet.settings.stubs(:use)
      @searcher = @file_class.new

      @cert = stub 'certificate', :name => "myname"
      @certpath = File.join(@path, "myname.pem")

      @request = stub 'request', :key => @cert.name, :instance => @cert
    end

    it "should consider the file a ca file if the name is equal to what the SSL::Host class says is the CA name" do
      Puppet::SSL::Host.expects(:ca_name).returns "amaca"
      @searcher.should be_ca("amaca")
    end

    describe "when choosing the location for certificates" do
      it "should set them at the ca setting's path if a ca setting is available and the name resolves to the CA name" do
        @file_class.store_in nil
        @file_class.store_at :mysetting
        @file_class.store_ca_at :cakey

        Puppet[:cakey] = File.expand_path("/ca/file")

        @searcher.expects(:ca?).with(@cert.name).returns true
        @searcher.path(@cert.name).should == Puppet[:cakey]
      end

      it "should set them at the file location if a file setting is available" do
        @file_class.store_in nil
        @file_class.store_at :cacrl

        Puppet[:cacrl] = File.expand_path("/some/file")

        @searcher.path(@cert.name).should == Puppet[:cacrl]
      end

      it "should set them in the setting directory, with the certificate name plus '.pem', if a directory setting is available" do
        @searcher.path(@cert.name).should == @certpath
      end

      ['../foo', '..\\foo', './../foo', '.\\..\\foo',
       '/foo', '//foo', '\\foo', '\\\\goo',
       "test\0/../bar", "test\0\\..\\bar",
       "..\\/bar", "/tmp/bar", "/tmp\\bar", "tmp\\bar",
       " / bar", " /../ bar", " \\..\\ bar",
       "c:\\foo", "c:/foo", "\\\\?\\UNC\\bar", "\\\\foo\\bar",
       "\\\\?\\c:\\foo", "//?/UNC/bar", "//foo/bar",
       "//?/c:/foo",
      ].each do |input|
        it "should resist directory traversal attacks (#{input.inspect})" do
          expect { @searcher.path(input) }.to raise_error
        end
      end

      # REVISIT: Should probably test MS-DOS reserved names here, too, since
      # they would represent a vulnerability on a Win32 system, should we ever
      # support that path.  Don't forget that 'CON.foo' == 'CON'
      # --daniel 2011-09-24
    end

    describe "when finding certificates on disk" do
      describe "and no certificate is present" do
        it "should return nil" do
          Puppet::FileSystem.expects(:exist?).with(@path).returns(true)
          Dir.expects(:entries).with(@path).returns([])
          Puppet::FileSystem.expects(:exist?).with(@certpath).returns(false)

          @searcher.find(@request).should be_nil
        end
      end

      describe "and a certificate is present" do
        let(:cert) { mock 'cert' }
        let(:model) { mock 'model' }

        before(:each) do
          @file_class.stubs(:model).returns model
        end

        context "is readable" do
          it "should return an instance of the model, which it should use to read the certificate" do
            Puppet::FileSystem.expects(:exist?).with(@certpath).returns true

            model.expects(:new).with("myname").returns cert
            cert.expects(:read).with(@certpath)

            @searcher.find(@request).should equal(cert)
          end
        end

        context "is unreadable" do
          it "should raise an exception" do
            Puppet::FileSystem.expects(:exist?).with(@certpath).returns(true)

            model.expects(:new).with("myname").returns cert
            cert.expects(:read).with(@certpath).raises(Errno::EACCES)

            expect {
              @searcher.find(@request)
            }.to raise_error(Errno::EACCES)
          end
        end
      end

      describe "and a certificate is present but has uppercase letters" do
        before do
          @request = stub 'request', :key => "myhost"
        end

        # This is kind of more an integration test; it's for #1382, until
        # the support for upper-case certs can be removed around mid-2009.
        it "should rename the existing file to the lower-case path" do
          @path = @searcher.path("myhost")
          Puppet::FileSystem.expects(:exist?).with(@path).returns(false)
          dir, file = File.split(@path)
          Puppet::FileSystem.expects(:exist?).with(dir).returns true
          Dir.expects(:entries).with(dir).returns [".", "..", "something.pem", file.upcase]

          File.expects(:rename).with(File.join(dir, file.upcase), @path)

          cert = mock 'cert'
          model = mock 'model'
          @searcher.stubs(:model).returns model
          @searcher.model.expects(:new).with("myhost").returns cert
          cert.expects(:read).with(@path)

          @searcher.find(@request)
        end
      end
    end

    describe "when saving certificates to disk" do
      before do
        FileTest.stubs(:directory?).returns true
        FileTest.stubs(:writable?).returns true
      end

      it "should fail if the directory is absent" do
        FileTest.expects(:directory?).with(File.dirname(@certpath)).returns false
        lambda { @searcher.save(@request) }.should raise_error(Puppet::Error)
      end

      it "should fail if the directory is not writeable" do
        FileTest.stubs(:directory?).returns true
        FileTest.expects(:writable?).with(File.dirname(@certpath)).returns false
        lambda { @searcher.save(@request) }.should raise_error(Puppet::Error)
      end

      it "should save to the path the output of converting the certificate to a string" do
        fh = mock 'filehandle'
        fh.expects(:print).with("mycert")

        @searcher.stubs(:write).yields fh
        @cert.expects(:to_s).returns "mycert"

        @searcher.save(@request)
      end

      describe "and a directory setting is set" do
        it "should use the Settings class to write the file" do
          @searcher.class.store_in @setting
          fh = mock 'filehandle'
          fh.stubs :print
          Puppet.settings.setting(@setting).expects(:open_file).with(@certpath, 'w').yields fh

          @searcher.save(@request)
        end
      end

      describe "and a file location is set" do
        it "should use the filehandle provided by the Settings" do
          @searcher.class.store_at @setting

          fh = mock 'filehandle'
          fh.stubs :print
          Puppet.settings.setting(@setting).expects(:open).with('w').yields fh
          @searcher.save(@request)
        end
      end

      describe "and the name is the CA name and a ca setting is set" do
        it "should use the filehandle provided by the Settings" do
          @searcher.class.store_at @setting
          @searcher.class.store_ca_at :cakey
          Puppet[:cakey] = "castuff stub"

          fh = mock 'filehandle'
          fh.stubs :print
          Puppet.settings.setting(:cakey).expects(:open).with('w').yields fh
          @searcher.stubs(:ca?).returns true
          @searcher.save(@request)
        end
      end
    end

    describe "when destroying certificates" do
      describe "that do not exist" do
        before do
          Puppet::FileSystem.expects(:exist?).with(Puppet::FileSystem.pathname(@certpath)).returns false
        end

        it "should return false" do
          @searcher.destroy(@request).should be_false
        end
      end

      describe "that exist" do
        it "should unlink the certificate file" do
          path = Puppet::FileSystem.pathname(@certpath)
          Puppet::FileSystem.expects(:exist?).with(path).returns true
          Puppet::FileSystem.expects(:unlink).with(path)
          @searcher.destroy(@request)
        end

        it "should log that is removing the file" do
          Puppet::FileSystem.stubs(:exist?).returns true
          Puppet::FileSystem.stubs(:unlink)
          Puppet.expects(:notice)
          @searcher.destroy(@request)
        end
      end
    end

    describe "when searching for certificates" do
      let(:one) { stub 'one' }
      let(:two) { stub 'two' }
      let(:one_path) { File.join(@path, 'one.pem') }
      let(:two_path) { File.join(@path, 'two.pem') }
      let(:model) { mock 'model' }

      before :each do
        @file_class.stubs(:model).returns model
      end

      it "should return a certificate instance for all files that exist" do
        Dir.expects(:entries).with(@path).returns(%w{. .. one.pem two.pem})

        model.expects(:new).with("one").returns one
        one.expects(:read).with(one_path)
        model.expects(:new).with("two").returns two
        two.expects(:read).with(two_path)

        @searcher.search(@request).should == [one, two]
      end

      it "should raise an exception if any file is unreadable" do
        Dir.expects(:entries).with(@path).returns(%w{. .. one.pem two.pem})

        model.expects(:new).with("one").returns(one)
        one.expects(:read).with(one_path)
        model.expects(:new).with("two").returns(two)
        two.expects(:read).raises(Errno::EACCES)

        expect {
          @searcher.search(@request)
        }.to raise_error(Errno::EACCES)
      end

      it "should skip any files that do not match /\.pem$/" do
        Dir.expects(:entries).with(@path).returns(%w{. .. one two.notpem})

        model.expects(:new).never

        @searcher.search(@request).should == []
      end
    end
  end
end