rkerberos 0.2.3 → 0.3.1

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.
@@ -36,22 +36,64 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
36
36
 
37
37
  subject(:keytab) { described_class.new }
38
38
 
39
+ def write_keytab_with_invalid_second_record(destination)
40
+ data = File.binread(@keytab_file)
41
+ first_record_size = data.byteslice(2, 4).unpack1('N')
42
+ second_record_offset = 6 + first_record_size
43
+
44
+ # INT32_MIN is reserved as an invalid record size by the FILE keytab
45
+ # implementation, so iteration must fail after yielding the first entry.
46
+ data[second_record_offset, 4] = [0x80000000].pack('N')
47
+ File.binwrite(destination, data)
48
+ end
49
+
39
50
  describe 'constructor' do
40
- it 'accepts an optional name' do
41
- expect { described_class.new("FILE:/usr/local/var/keytab") }.not_to raise_error
42
- expect { described_class.new("FILE:/bogus/keytab") }.not_to raise_error
51
+ it 'accepts an optional name keyword' do
52
+ expect { described_class.new(name: "FILE:/usr/local/var/keytab") }.not_to raise_error
53
+ expect { described_class.new(name: "FILE:/bogus/keytab") }.not_to raise_error
43
54
  end
44
55
 
45
56
  it 'raises error for invalid residual type' do
46
57
  expect {
47
- described_class.new("BOGUS:/tmp/keytab")
58
+ described_class.new(name: "BOGUS:/tmp/keytab")
48
59
  }.to raise_error(Kerberos::Krb5::Keytab::Exception)
49
60
  end
61
+
62
+ it 'accepts a context keyword argument' do
63
+ ctx = Kerberos::Krb5::Context.new
64
+ keytab = described_class.new(name: @keytab_name, context: ctx)
65
+ expect { ctx.close }.to raise_error(Kerberos::Krb5::Exception, /dependent wrapper/)
66
+ keytab.close
67
+ expect { ctx.close }.not_to raise_error
68
+ end
69
+
70
+ it 'works with context and no name' do
71
+ ctx = Kerberos::Krb5::Context.new
72
+ expect { described_class.new(context: ctx) }.not_to raise_error
73
+ end
74
+
75
+ it 'rejects positional arguments' do
76
+ expect { described_class.new(@keytab_name) }.to raise_error(ArgumentError)
77
+ end
78
+
79
+ it 'rejects unknown keywords' do
80
+ expect { described_class.new(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
81
+ end
82
+
83
+ it 'raises TypeError for non-Context context argument' do
84
+ expect { described_class.new(context: "bad") }.to raise_error(TypeError)
85
+ end
86
+
87
+ it 'raises error for a closed context' do
88
+ ctx = Kerberos::Krb5::Context.new
89
+ ctx.close
90
+ expect { described_class.new(context: ctx) }.to raise_error(Kerberos::Krb5::Exception)
91
+ end
50
92
  end
51
93
 
52
94
  describe '#keytab_name and #keytab_type' do
53
95
  it 'returns the underlying name and type strings' do
54
- kt = described_class.new(@keytab_name)
96
+ kt = described_class.new(name: @keytab_name)
55
97
  expect(kt).to respond_to(:keytab_name)
56
98
  expect(kt).to respond_to(:keytab_type)
57
99
 
@@ -67,34 +109,43 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
67
109
 
68
110
  describe '#close' do
69
111
  it 'returns true' do
70
- kt = described_class.new(@keytab_name)
112
+ kt = described_class.new(name: @keytab_name)
71
113
  expect(kt.close).to eq(true)
72
114
  end
73
115
 
74
116
  it 'can be called multiple times without error' do
75
- kt = described_class.new(@keytab_name)
117
+ kt = described_class.new(name: @keytab_name)
76
118
  kt.close
77
119
  expect { kt.close }.not_to raise_error
78
120
  end
79
121
 
80
122
  it 'raises an error when calling keytab_name after close' do
81
- kt = described_class.new(@keytab_name)
123
+ kt = described_class.new(name: @keytab_name)
82
124
  kt.close
83
125
  expect { kt.keytab_name }.to raise_error(Kerberos::Krb5::Exception)
84
126
  end
85
127
 
86
128
  it 'raises an error when calling keytab_type after close' do
87
- kt = described_class.new(@keytab_name)
129
+ kt = described_class.new(name: @keytab_name)
88
130
  kt.close
89
131
  expect { kt.keytab_type }.to raise_error(Kerberos::Krb5::Exception)
90
132
  end
91
133
 
92
134
  it 'does not segfault when garbage collected after close' do
93
- kt = described_class.new(@keytab_name)
135
+ kt = described_class.new(name: @keytab_name)
94
136
  kt.close
95
137
  kt = nil
96
138
  GC.start
97
139
  end
140
+
141
+ it 'raises from every operation that requires the closed handle' do
142
+ kt = described_class.new(name: @keytab_name)
143
+ kt.close
144
+
145
+ expect { kt.default_name }.to raise_error(Kerberos::Krb5::Exception)
146
+ expect { kt.each { |_| } }.to raise_error(Kerberos::Krb5::Exception)
147
+ expect { kt.get_entry("testuser1@#{@realm}") }.to raise_error(Kerberos::Krb5::Exception)
148
+ end
98
149
  end
99
150
 
100
151
  describe '.foreach' do
@@ -129,6 +180,19 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
129
180
  }.to raise_error(Kerberos::Krb5::Exception)
130
181
  end
131
182
 
183
+ it 'raises when an error interrupts iteration' do
184
+ corrupt_keytab = File.join(Dir.tmpdir, "invalid-foreach-#{Process.pid}.keytab")
185
+ write_keytab_with_invalid_second_record(corrupt_keytab)
186
+ entries = []
187
+
188
+ expect {
189
+ described_class.foreach("FILE:#{corrupt_keytab}") { |entry| entries << entry }
190
+ }.to raise_error(Kerberos::Krb5::Exception, /krb5_kt_next_entry/)
191
+ expect(entries).not_to be_empty
192
+ ensure
193
+ FileUtils.rm_f(corrupt_keytab)
194
+ end
195
+
132
196
  it 'raises an error for an invalid keytab type' do
133
197
  expect {
134
198
  described_class.foreach("BOGUS:/tmp/keytab") { |_| }
@@ -152,7 +216,7 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
152
216
 
153
217
  describe '#each' do
154
218
  it 'yields keytab entries for a valid keytab' do
155
- kt = described_class.new(@keytab_name)
219
+ kt = described_class.new(name: @keytab_name)
156
220
  entries = []
157
221
  kt.each { |entry| entries << entry }
158
222
  expect(entries.length).to eq(2)
@@ -166,25 +230,40 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
166
230
  end
167
231
 
168
232
  it 'does not leak resources when the block raises' do
169
- kt = described_class.new(@keytab_name)
233
+ kt = described_class.new(name: @keytab_name)
170
234
  expect {
171
235
  kt.each { |_| raise "boom" }
172
236
  }.to raise_error(RuntimeError, "boom")
173
237
  end
174
238
 
175
239
  it 'does not leak resources when the block breaks' do
176
- kt = described_class.new(@keytab_name)
240
+ kt = described_class.new(name: @keytab_name)
177
241
  result = catch(:done) do
178
242
  kt.each { |_| throw :done, :escaped }
179
243
  :completed
180
244
  end
181
245
  expect(result).to eq(:escaped)
182
246
  end
247
+
248
+ it 'raises instead of returning a partial result after an iteration error' do
249
+ corrupt_keytab = File.join(Dir.tmpdir, "invalid-each-#{Process.pid}.keytab")
250
+ write_keytab_with_invalid_second_record(corrupt_keytab)
251
+ entries = []
252
+ kt = described_class.new(name: "FILE:#{corrupt_keytab}")
253
+
254
+ expect {
255
+ kt.each { |entry| entries << entry }
256
+ }.to raise_error(Kerberos::Krb5::Exception, /krb5_kt_next_entry/)
257
+ expect(entries).not_to be_empty
258
+ ensure
259
+ kt&.close
260
+ FileUtils.rm_f(corrupt_keytab)
261
+ end
183
262
  end
184
263
 
185
264
  describe '#get_entry' do
186
265
  it 'finds an entry by principal name' do
187
- kt = described_class.new(@keytab_name)
266
+ kt = described_class.new(name: @keytab_name)
188
267
  entry = kt.get_entry("testuser1@#{@realm}")
189
268
  expect(entry).to be_a(Kerberos::Krb5::Keytab::Entry)
190
269
  expect(entry.principal).to eq("testuser1@#{@realm}")
@@ -194,14 +273,14 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
194
273
  end
195
274
 
196
275
  it 'finds an entry filtering by vno' do
197
- kt = described_class.new(@keytab_name)
276
+ kt = described_class.new(name: @keytab_name)
198
277
  entry = kt.get_entry("testuser1@#{@realm}", 1)
199
278
  expect(entry.principal).to eq("testuser1@#{@realm}")
200
279
  expect(entry.vno).to eq(1)
201
280
  end
202
281
 
203
282
  it 'finds an entry filtering by vno and enctype' do
204
- kt = described_class.new(@keytab_name)
283
+ kt = described_class.new(name: @keytab_name)
205
284
  # aes128-cts-hmac-sha1-96 is enctype 17
206
285
  entry = kt.get_entry("testuser1@#{@realm}", 1, 17)
207
286
  expect(entry.principal).to eq("testuser1@#{@realm}")
@@ -210,21 +289,21 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
210
289
  end
211
290
 
212
291
  it 'raises an error for a non-existent principal' do
213
- kt = described_class.new(@keytab_name)
292
+ kt = described_class.new(name: @keytab_name)
214
293
  expect {
215
294
  kt.get_entry("bogus@#{@realm}")
216
295
  }.to raise_error(Kerberos::Krb5::Exception)
217
296
  end
218
297
 
219
298
  it 'is aliased as find' do
220
- kt = described_class.new(@keytab_name)
299
+ kt = described_class.new(name: @keytab_name)
221
300
  expect(kt.method(:find)).to eq(kt.method(:get_entry))
222
301
  end
223
302
  end
224
303
 
225
304
  describe '#dup' do
226
305
  it 'creates an independent handle referring to same keytab' do
227
- kt1 = described_class.new(@keytab_name)
306
+ kt1 = described_class.new(name: @keytab_name)
228
307
  kt2 = kt1.dup
229
308
  expect(kt2).to be_a(described_class)
230
309
  expect(kt2.keytab_name).to eq(kt1.keytab_name)
@@ -235,8 +314,184 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
235
314
  end
236
315
 
237
316
  it 'clone is an alias for dup' do
238
- kt = described_class.new(@keytab_name)
317
+ kt = described_class.new(name: @keytab_name)
239
318
  expect(kt.method(:clone)).to eq(kt.method(:dup))
240
319
  end
241
320
  end
321
+
322
+ describe '#have_content?' do
323
+ it 'returns true for a keytab with entries' do
324
+ kt = described_class.new(name: @keytab_name)
325
+ expect(kt.have_content?).to be true
326
+ kt.close
327
+ end
328
+
329
+ it 'returns false for an empty keytab' do
330
+ empty_file = File.join(Dir.tmpdir, 'empty_test.keytab')
331
+ FileUtils.touch(empty_file)
332
+ kt = described_class.new(name: "FILE:#{empty_file}")
333
+ expect(kt.have_content?).to be false
334
+ kt.close
335
+ FileUtils.rm_f(empty_file)
336
+ end
337
+
338
+ it 'raises when the context is closed' do
339
+ kt = described_class.new(name: @keytab_name)
340
+ kt.close
341
+ expect { kt.have_content? }.to raise_error(Kerberos::Krb5::Exception)
342
+ end
343
+ end
344
+
345
+ describe '#add_entry' do
346
+ let(:add_kt_file) { File.join(Dir.tmpdir, 'add_test.keytab') }
347
+ let(:add_kt_name) { "FILE:#{add_kt_file}" }
348
+
349
+ after(:each) { FileUtils.rm_f(add_kt_file) }
350
+
351
+ it 'adds an entry that is retrievable via get_entry' do
352
+ kt = described_class.new(name: add_kt_name)
353
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1)
354
+
355
+ entry = kt.get_entry("testuser1@#{@realm}")
356
+ expect(entry).to be_a(Kerberos::Krb5::Keytab::Entry)
357
+ expect(entry.principal).to eq("testuser1@#{@realm}")
358
+ expect(entry.vno).to eq(1)
359
+ # default enctype is aes256-cts-hmac-sha1-96 = 18
360
+ expect(entry.key).to eq(18)
361
+ kt.close
362
+ end
363
+
364
+ it 'accepts an explicit enctype' do
365
+ kt = described_class.new(name: add_kt_name)
366
+ # aes128-cts-hmac-sha1-96 = 17
367
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 2, enctype: 17)
368
+
369
+ entry = kt.get_entry("testuser1@#{@realm}", 2, 17)
370
+ expect(entry.vno).to eq(2)
371
+ expect(entry.key).to eq(17)
372
+ kt.close
373
+ end
374
+
375
+ it 'can add multiple entries for the same principal' do
376
+ kt = described_class.new(name: add_kt_name)
377
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 18)
378
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 17)
379
+
380
+ entries = []
381
+ kt.each { |e| entries << e }
382
+ expect(entries.length).to eq(2)
383
+ kt.close
384
+ end
385
+
386
+ it 'returns self for chaining' do
387
+ kt = described_class.new(name: add_kt_name)
388
+ result = kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret')
389
+ expect(result).to equal(kt)
390
+ kt.close
391
+ end
392
+
393
+ it 'raises ArgumentError when principal is missing' do
394
+ kt = described_class.new(name: add_kt_name)
395
+ expect { kt.add_entry(password: 'secret') }.to raise_error(ArgumentError)
396
+ kt.close
397
+ end
398
+
399
+ it 'raises ArgumentError when password is missing' do
400
+ kt = described_class.new(name: add_kt_name)
401
+ expect { kt.add_entry(principal: "testuser1@#{@realm}") }.to raise_error(ArgumentError)
402
+ kt.close
403
+ end
404
+
405
+ it 'raises ArgumentError with no arguments' do
406
+ kt = described_class.new(name: add_kt_name)
407
+ expect { kt.add_entry }.to raise_error(ArgumentError)
408
+ kt.close
409
+ end
410
+
411
+ it 'rejects unknown keywords' do
412
+ kt = described_class.new(name: add_kt_name)
413
+ expect {
414
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', unknown: true)
415
+ }.to raise_error(ArgumentError, /unknown keyword/)
416
+ kt.close
417
+ end
418
+
419
+ it 'raises when the keytab is closed' do
420
+ kt = described_class.new(name: add_kt_name)
421
+ kt.close
422
+ expect {
423
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret')
424
+ }.to raise_error(Kerberos::Krb5::Exception)
425
+ end
426
+ end
427
+
428
+ describe '#remove_entry' do
429
+ let(:rm_kt_file) { File.join(Dir.tmpdir, 'remove_test.keytab') }
430
+ let(:rm_kt_name) { "FILE:#{rm_kt_file}" }
431
+
432
+ before(:each) do
433
+ @rm_kt = described_class.new(name: rm_kt_name)
434
+ @rm_kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 18)
435
+ @rm_kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 2, enctype: 17)
436
+ @rm_kt.add_entry(principal: "testuser2@#{@realm}", password: 'secret', vno: 1, enctype: 18)
437
+ end
438
+
439
+ after(:each) do
440
+ @rm_kt.close rescue nil
441
+ FileUtils.rm_f(rm_kt_file)
442
+ end
443
+
444
+ it 'removes all entries for a principal when vno and enctype are defaulted' do
445
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}")
446
+ entries = []
447
+ @rm_kt.each { |e| entries << e }
448
+ principals = entries.map(&:principal)
449
+ expect(principals).not_to include("testuser1@#{@realm}")
450
+ expect(principals).to include("testuser2@#{@realm}")
451
+ end
452
+
453
+ it 'removes only the entry matching a specific vno' do
454
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}", vno: 1)
455
+ entry = @rm_kt.get_entry("testuser1@#{@realm}")
456
+ expect(entry.vno).to eq(2)
457
+ end
458
+
459
+ it 'removes only the entry matching a specific enctype' do
460
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}", enctype: 17)
461
+ entry = @rm_kt.get_entry("testuser1@#{@realm}")
462
+ expect(entry.key).to eq(18)
463
+ end
464
+
465
+ it 'returns self for chaining' do
466
+ result = @rm_kt.remove_entry(principal: "testuser2@#{@realm}")
467
+ expect(result).to equal(@rm_kt)
468
+ end
469
+
470
+ it 'raises ArgumentError when principal is missing' do
471
+ expect { @rm_kt.remove_entry(vno: 1) }.to raise_error(ArgumentError)
472
+ end
473
+
474
+ it 'raises ArgumentError with no arguments' do
475
+ expect { @rm_kt.remove_entry }.to raise_error(ArgumentError)
476
+ end
477
+
478
+ it 'rejects unknown keywords' do
479
+ expect {
480
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}", unknown: true)
481
+ }.to raise_error(ArgumentError, /unknown keyword/)
482
+ end
483
+
484
+ it 'raises when the keytab is closed' do
485
+ @rm_kt.close
486
+ expect {
487
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}")
488
+ }.to raise_error(Kerberos::Krb5::Exception)
489
+ end
490
+
491
+ it 'raises for a non-existent entry' do
492
+ expect {
493
+ @rm_kt.remove_entry(principal: "nobody@#{@realm}")
494
+ }.to raise_error(Kerberos::Krb5::Keytab::Exception)
495
+ end
496
+ end
242
497
  end