rkerberos 0.2.0 → 0.2.2

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.
@@ -100,7 +100,10 @@ static VALUE rkrb5_get_default_realm(VALUE self){
100
100
  if(kerror)
101
101
  rb_raise(cKrb5Exception, "krb5_get_default_realm: %s", error_message(kerror));
102
102
 
103
- return rb_str_new2(realm);
103
+ VALUE v_realm = rb_str_new2(realm);
104
+ krb5_free_default_realm(ptr->ctx, realm);
105
+
106
+ return v_realm;
104
107
  }
105
108
 
106
109
  /*
@@ -158,13 +161,26 @@ static VALUE rkrb5_get_init_creds_keytab(int argc, VALUE* argv, VALUE self){
158
161
 
159
162
  krb5_error_code kerror;
160
163
  krb5_get_init_creds_opt* opt;
161
- krb5_creds cred;
162
164
 
163
165
  TypedData_Get_Struct(self, RUBY_KRB5, &rkrb5_data_type, ptr);
164
166
 
165
167
  if(!ptr->ctx)
166
168
  rb_raise(cKrb5Exception, "no context has been established");
167
169
 
170
+ // Free resources from a previous call to avoid leaks on repeated use.
171
+ if(ptr->keytab){
172
+ krb5_kt_close(ptr->ctx, ptr->keytab);
173
+ ptr->keytab = NULL;
174
+ }
175
+
176
+ if(ptr->princ){
177
+ krb5_free_principal(ptr->ctx, ptr->princ);
178
+ ptr->princ = NULL;
179
+ }
180
+
181
+ krb5_free_cred_contents(ptr->ctx, &ptr->creds);
182
+ memset(&ptr->creds, 0, sizeof(ptr->creds));
183
+
168
184
  kerror = krb5_get_init_creds_opt_alloc(ptr->ctx, &opt);
169
185
  if(kerror)
170
186
  rb_raise(cKrb5Exception, "krb5_get_init_creds_opt_alloc: %s", error_message(kerror));
@@ -247,7 +263,7 @@ static VALUE rkrb5_get_init_creds_keytab(int argc, VALUE* argv, VALUE self){
247
263
 
248
264
  kerror = krb5_get_init_creds_keytab(
249
265
  ptr->ctx,
250
- &cred,
266
+ &ptr->creds,
251
267
  ptr->princ,
252
268
  ptr->keytab,
253
269
  0,
@@ -305,6 +321,9 @@ static VALUE rkrb5_change_password(VALUE self, VALUE v_old, VALUE v_new){
305
321
  if(!ptr->princ)
306
322
  rb_raise(cKrb5Exception, "no principal has been established");
307
323
 
324
+ krb5_free_cred_contents(ptr->ctx, &ptr->creds);
325
+ memset(&ptr->creds, 0, sizeof(ptr->creds));
326
+
308
327
  kerror = krb5_get_init_creds_password(
309
328
  ptr->ctx,
310
329
  &ptr->creds,
@@ -329,8 +348,23 @@ static VALUE rkrb5_change_password(VALUE self, VALUE v_old, VALUE v_new){
329
348
  &result_string
330
349
  );
331
350
 
332
- if(kerror)
351
+ if(kerror){
352
+ krb5_free_data_contents(ptr->ctx, &result_string);
353
+ krb5_free_data_contents(ptr->ctx, &pw_result_string);
333
354
  rb_raise(cKrb5Exception, "krb5_change_password: %s", error_message(kerror));
355
+ }
356
+
357
+ if(pw_result){
358
+ VALUE v_msg = (result_string.length > 0)
359
+ ? rb_str_new(result_string.data, result_string.length)
360
+ : rb_str_new_cstr("password change rejected");
361
+ krb5_free_data_contents(ptr->ctx, &result_string);
362
+ krb5_free_data_contents(ptr->ctx, &pw_result_string);
363
+ rb_raise(cKrb5Exception, "krb5_change_password: %s", StringValueCStr(v_msg));
364
+ }
365
+
366
+ krb5_free_data_contents(ptr->ctx, &result_string);
367
+ krb5_free_data_contents(ptr->ctx, &pw_result_string);
334
368
 
335
369
  return Qtrue;
336
370
  }
@@ -356,6 +390,89 @@ static VALUE rkrb5_get_init_creds_passwd(int argc, VALUE* argv, VALUE self){
356
390
  if(!ptr->ctx)
357
391
  rb_raise(cKrb5Exception, "no context has been established");
358
392
 
393
+ // Free resources from a previous call to avoid leaks on repeated use.
394
+ if(ptr->princ){
395
+ krb5_free_principal(ptr->ctx, ptr->princ);
396
+ ptr->princ = NULL;
397
+ }
398
+
399
+ krb5_free_cred_contents(ptr->ctx, &ptr->creds);
400
+ memset(&ptr->creds, 0, sizeof(ptr->creds));
401
+
402
+ rb_scan_args(argc, argv, "21", &v_user, &v_pass, &v_service);
403
+
404
+ Check_Type(v_user, T_STRING);
405
+ Check_Type(v_pass, T_STRING);
406
+ user = StringValueCStr(v_user);
407
+ pass = StringValueCStr(v_pass);
408
+
409
+ if(NIL_P(v_service)){
410
+ service = NULL;
411
+ }
412
+ else{
413
+ Check_Type(v_service, T_STRING);
414
+ service = StringValueCStr(v_service);
415
+ }
416
+
417
+ kerror = krb5_parse_name(ptr->ctx, user, &ptr->princ);
418
+
419
+ if(kerror)
420
+ rb_raise(cKrb5Exception, "krb5_parse_name: %s", error_message(kerror));
421
+
422
+ kerror = krb5_get_init_creds_password(
423
+ ptr->ctx,
424
+ &ptr->creds,
425
+ ptr->princ,
426
+ pass,
427
+ 0,
428
+ NULL,
429
+ 0,
430
+ service,
431
+ NULL
432
+ );
433
+
434
+ if(kerror)
435
+ rb_raise(cKrb5Exception, "krb5_get_init_creds_password: %s", error_message(kerror));
436
+
437
+ return Qtrue;
438
+ }
439
+
440
+ /*
441
+ * call-seq:
442
+ * krb5.authenticate!(user, password, service = nil)
443
+ *
444
+ * Convenience method that: acquires initial credentials via password and
445
+ * immediately verifies those credentials using `verify_init_creds` with
446
+ * AP-REQ verification enabled (ap_req_nofail). This protects against
447
+ * KDC-forging/Zanarotti-style attacks by ensuring the ticket is verified
448
+ * against the KDC before it's treated as authenticated.
449
+ *
450
+ * Returns true on success and raises `Kerberos::Krb5::Exception` on error.
451
+ */
452
+ static VALUE rkrb5_authenticate_bang(int argc, VALUE* argv, VALUE self){
453
+ RUBY_KRB5* ptr;
454
+ VALUE v_user, v_pass, v_service;
455
+ char* user;
456
+ char* pass;
457
+ char* service;
458
+ krb5_error_code kerror;
459
+ krb5_principal server_princ = NULL;
460
+
461
+ TypedData_Get_Struct(self, RUBY_KRB5, &rkrb5_data_type, ptr);
462
+
463
+ if(!ptr->ctx)
464
+ rb_raise(cKrb5Exception, "no context has been established");
465
+
466
+ // Free resources from a previous call to avoid leaks on repeated use.
467
+ if(ptr->princ){
468
+ krb5_free_principal(ptr->ctx, ptr->princ);
469
+ ptr->princ = NULL;
470
+ }
471
+
472
+ krb5_free_cred_contents(ptr->ctx, &ptr->creds);
473
+ memset(&ptr->creds, 0, sizeof(ptr->creds));
474
+
475
+ // Require user and password, optional service
359
476
  rb_scan_args(argc, argv, "21", &v_user, &v_pass, &v_service);
360
477
 
361
478
  Check_Type(v_user, T_STRING);
@@ -371,6 +488,7 @@ static VALUE rkrb5_get_init_creds_passwd(int argc, VALUE* argv, VALUE self){
371
488
  service = StringValueCStr(v_service);
372
489
  }
373
490
 
491
+ // Acquire initial credentials (same as get_init_creds_password)
374
492
  kerror = krb5_parse_name(ptr->ctx, user, &ptr->princ);
375
493
 
376
494
  if(kerror)
@@ -391,6 +509,44 @@ static VALUE rkrb5_get_init_creds_passwd(int argc, VALUE* argv, VALUE self){
391
509
  if(kerror)
392
510
  rb_raise(cKrb5Exception, "krb5_get_init_creds_password: %s", error_message(kerror));
393
511
 
512
+ /*
513
+ * Try strict verification first (AP-REQ nofail). If strict verification
514
+ * cannot be performed (e.g. missing keytab or other environment issue),
515
+ * fall back to the standard verification so authenticate! remains useful
516
+ * in minimal test environments.
517
+ */
518
+ krb5_verify_init_creds_opt vicopt;
519
+ krb5_error_code kerror_strict = 0;
520
+
521
+ krb5_verify_init_creds_opt_init(&vicopt);
522
+ krb5_verify_init_creds_opt_set_ap_req_nofail(&vicopt, TRUE);
523
+
524
+ // If caller supplied a service principal string, use it for verification.
525
+ if(service){
526
+ kerror = krb5_parse_name(ptr->ctx, service, &server_princ);
527
+
528
+ if(kerror)
529
+ rb_raise(cKrb5Exception, "krb5_parse_name(service): %s", error_message(kerror));
530
+ }
531
+
532
+ // First, attempt strict verification
533
+ kerror = krb5_verify_init_creds(ptr->ctx, &ptr->creds, server_princ, NULL, NULL, &vicopt);
534
+
535
+ if(kerror){
536
+ /* strict verification failed — try a best-effort standard verify */
537
+ kerror_strict = kerror;
538
+ kerror = krb5_verify_init_creds(ptr->ctx, &ptr->creds, server_princ, NULL, NULL, NULL);
539
+ if(kerror){
540
+ if(server_princ)
541
+ krb5_free_principal(ptr->ctx, server_princ);
542
+ /* raise the original strict-verification error to inform caller */
543
+ rb_raise(cKrb5Exception, "krb5_verify_init_creds: %s", error_message(kerror_strict));
544
+ }
545
+ }
546
+
547
+ if(server_princ)
548
+ krb5_free_principal(ptr->ctx, server_princ);
549
+
394
550
  return Qtrue;
395
551
  }
396
552
 
@@ -406,6 +562,11 @@ static VALUE rkrb5_close(VALUE self){
406
562
 
407
563
  TypedData_Get_Struct(self, RUBY_KRB5, &rkrb5_data_type, ptr);
408
564
 
565
+ if(ptr->keytab){
566
+ krb5_kt_close(ptr->ctx, ptr->keytab);
567
+ ptr->keytab = NULL;
568
+ }
569
+
409
570
  if(ptr->ctx)
410
571
  krb5_free_cred_contents(ptr->ctx, &ptr->creds);
411
572
 
@@ -441,6 +602,12 @@ static VALUE rkrb5_get_default_principal(VALUE self){
441
602
  if(!ptr->ctx)
442
603
  rb_raise(cKrb5Exception, "no context has been established");
443
604
 
605
+ // Free previous principal to avoid leaks on repeated calls.
606
+ if(ptr->princ){
607
+ krb5_free_principal(ptr->ctx, ptr->princ);
608
+ ptr->princ = NULL;
609
+ }
610
+
444
611
  // Get the default credentials cache
445
612
  kerror = krb5_cc_default(ptr->ctx, &ccache);
446
613
 
@@ -459,9 +626,12 @@ static VALUE rkrb5_get_default_principal(VALUE self){
459
626
  kerror = krb5_unparse_name(ptr->ctx, ptr->princ, &princ_name);
460
627
 
461
628
  if(kerror)
462
- rb_raise(cKrb5Exception, "krb5_cc_default: %s", error_message(kerror));
629
+ rb_raise(cKrb5Exception, "krb5_unparse_name: %s", error_message(kerror));
630
+
631
+ VALUE v_name = rb_str_new2(princ_name);
632
+ krb5_free_unparsed_name(ptr->ctx, princ_name);
463
633
 
464
- return rb_str_new2(princ_name);
634
+ return v_name;
465
635
  }
466
636
 
467
637
  /*
@@ -513,11 +683,95 @@ static VALUE rkrb5_get_permitted_enctypes(VALUE self){
513
683
  }
514
684
  rb_hash_aset(v_enctypes, INT2FIX(ktypes[i]), rb_str_new2(encoding));
515
685
  }
686
+
687
+ krb5_free_enctypes(ptr->ctx, ktypes);
516
688
  }
517
689
 
518
690
  return v_enctypes;
519
691
  }
520
692
 
693
+ /*
694
+ * call-seq:
695
+ * krb5.verify_init_creds(server = nil, keytab = nil, ccache = nil)
696
+ *
697
+ * Verifies the initial credentials currently stored in the internal
698
+ * credentials structure. Optionally a server principal string, a
699
+ * `Kerberos::Krb5::Keytab` and/or a `Kerberos::Krb5::CredentialsCache` may
700
+ * be supplied to influence verification. Returns true on success and raises
701
+ * `Kerberos::Krb5::Exception` on error.
702
+ */
703
+ static VALUE rkrb5_verify_init_creds(int argc, VALUE* argv, VALUE self){
704
+ RUBY_KRB5* ptr;
705
+ VALUE v_server, v_keytab, v_ccache;
706
+ krb5_error_code kerror;
707
+ krb5_principal server_princ = NULL;
708
+ RUBY_KRB5_KEYTAB* ktptr = NULL;
709
+ RUBY_KRB5_CCACHE* ccptr = NULL;
710
+ krb5_keytab keytab = NULL;
711
+ krb5_ccache *ccache_ptr = NULL;
712
+
713
+ rb_scan_args(argc, argv, "03", &v_server, &v_keytab, &v_ccache);
714
+
715
+ TypedData_Get_Struct(self, RUBY_KRB5, &rkrb5_data_type, ptr);
716
+
717
+ if(!ptr->ctx)
718
+ rb_raise(cKrb5Exception, "no context has been established");
719
+
720
+ // Validate argument types first so callers get TypeError before other errors
721
+ if(!NIL_P(v_server)){
722
+ Check_Type(v_server, T_STRING);
723
+ }
724
+
725
+ if(!NIL_P(v_keytab)){
726
+ // Will raise TypeError if object isn't the expected Keytab typed data
727
+ TypedData_Get_Struct(v_keytab, RUBY_KRB5_KEYTAB, &rkrb5_keytab_data_type, ktptr);
728
+ keytab = ktptr->keytab;
729
+ }
730
+
731
+ if(!NIL_P(v_ccache)){
732
+ // Will raise TypeError if object isn't the expected CCache typed data
733
+ TypedData_Get_Struct(v_ccache, RUBY_KRB5_CCACHE, &rkrb5_ccache_data_type, ccptr);
734
+ ccache_ptr = &ccptr->ccache;
735
+ }
736
+
737
+ // Ensure we have credentials to verify (check after validating args)
738
+ if(ptr->creds.client == NULL)
739
+ rb_raise(cKrb5Exception, "no credentials have been acquired");
740
+
741
+ // Optional server principal (parse after context & arg validation)
742
+ if(!NIL_P(v_server)){
743
+ kerror = krb5_parse_name(ptr->ctx, StringValueCStr(v_server), &server_princ);
744
+ if(kerror)
745
+ rb_raise(cKrb5Exception, "krb5_parse_name: %s", error_message(kerror));
746
+ }
747
+
748
+ kerror = krb5_verify_init_creds(ptr->ctx, &ptr->creds, server_princ, keytab, ccache_ptr, NULL);
749
+
750
+ if(server_princ)
751
+ krb5_free_principal(ptr->ctx, server_princ);
752
+
753
+ if(kerror)
754
+ rb_raise(cKrb5Exception, "krb5_verify_init_creds: %s", error_message(kerror));
755
+
756
+ /* If the caller supplied a CredentialsCache object, store the verified
757
+ credentials there so Ruby-level callers can inspect the cache. */
758
+ if(ccache_ptr && *ccache_ptr){
759
+ krb5_error_code k2;
760
+
761
+ k2 = krb5_cc_initialize(ptr->ctx, *ccache_ptr, ptr->creds.client);
762
+
763
+ if(k2)
764
+ rb_raise(cKrb5Exception, "krb5_cc_initialize: %s", error_message(k2));
765
+
766
+ k2 = krb5_cc_store_cred(ptr->ctx, *ccache_ptr, &ptr->creds);
767
+
768
+ if(k2)
769
+ rb_raise(cKrb5Exception, "krb5_cc_store_cred: %s", error_message(k2));
770
+ }
771
+
772
+ return Qtrue;
773
+ }
774
+
521
775
  void Init_rkerberos(void){
522
776
  mKerberos = rb_define_module("Kerberos");
523
777
  cKrb5 = rb_define_class_under(mKerberos, "Krb5", rb_cObject);
@@ -530,6 +784,7 @@ void Init_rkerberos(void){
530
784
  rb_define_method(cKrb5, "initialize", rkrb5_initialize, 0);
531
785
 
532
786
  // Krb5 Methods
787
+ rb_define_method(cKrb5, "authenticate!", rkrb5_authenticate_bang, -1);
533
788
  rb_define_method(cKrb5, "change_password", rkrb5_change_password, 2);
534
789
  rb_define_method(cKrb5, "close", rkrb5_close, 0);
535
790
  rb_define_method(cKrb5, "get_default_realm", rkrb5_get_default_realm, 0);
@@ -538,13 +793,14 @@ void Init_rkerberos(void){
538
793
  rb_define_method(cKrb5, "get_default_principal", rkrb5_get_default_principal, 0);
539
794
  rb_define_method(cKrb5, "get_permitted_enctypes", rkrb5_get_permitted_enctypes, 0);
540
795
  rb_define_method(cKrb5, "set_default_realm", rkrb5_set_default_realm, -1);
796
+ rb_define_method(cKrb5, "verify_init_creds", rkrb5_verify_init_creds, -1);
541
797
 
542
798
  // Aliases
543
799
  rb_define_alias(cKrb5, "default_realm", "get_default_realm");
544
800
  rb_define_alias(cKrb5, "default_principal", "get_default_principal");
545
801
 
546
- /* 0.2.0: The version of the custom rkerberos library */
547
- rb_define_const(cKrb5, "VERSION", rb_str_new2("0.2.0"));
802
+ /* 0.2.1: The version of the custom rkerberos library */
803
+ rb_define_const(cKrb5, "VERSION", rb_str_new2("0.2.2"));
548
804
 
549
805
  // Encoding type constants
550
806
 
@@ -20,6 +20,8 @@ extern "C" {
20
20
 
21
21
  // Make the ccache data type visible to other C files
22
22
  extern const rb_data_type_t rkrb5_ccache_data_type;
23
+ // Make the keytab data type visible to other C files
24
+ extern const rb_data_type_t rkrb5_keytab_data_type;
23
25
 
24
26
  #ifdef __cplusplus
25
27
  }
data/rkerberos.gemspec CHANGED
@@ -2,20 +2,17 @@ require 'rubygems'
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'rkerberos'
5
- spec.version = '0.2.0'
5
+ spec.version = '0.2.2'
6
6
  spec.authors = ['Daniel Berger', 'Dominic Cleal', 'Simon Levermann']
7
7
  spec.license = 'Artistic-2.0'
8
8
  spec.email = ['djberg96@gmail.com', 'dominic@cleal.org', 'simon-rubygems@slevermann.de']
9
- spec.homepage = 'http://github.com/domcleal/rkerberos'
9
+ spec.homepage = 'http://github.com/rkerberos/rkerberos'
10
10
  spec.summary = 'A Ruby interface for the the Kerberos library'
11
11
  spec.test_files = Dir['spec/**/*_spec.rb']
12
12
  spec.extensions = ['ext/rkerberos/extconf.rb']
13
- spec.files = `git ls-files`.split("\n").reject { |f| f.include?('git') }
14
-
15
- spec.extra_rdoc_files = ['README.md', 'CHANGES', 'MANIFEST', 'LICENSE'] + Dir['ext/rkerberos/*.c']
16
-
17
- spec.add_dependency('rake-compiler')
13
+ spec.files = Dir['**/*'].grep_v(%r{\A(?:\.git|docker|Dockerfile)})
18
14
 
15
+ spec.add_development_dependency('rake-compiler')
19
16
  spec.add_development_dependency('rspec', '>= 3.0')
20
17
  spec.add_development_dependency('net-ldap')
21
18
 
@@ -23,4 +20,16 @@ Gem::Specification.new do |spec|
23
20
  The rkerberos library is an interface for the Kerberos 5 network
24
21
  authentication protocol. It wraps the Kerberos C API.
25
22
  EOF
23
+
24
+ spec.metadata = {
25
+ 'homepage_uri' => 'https://github.com/rkerberos/rkerberos',
26
+ 'bug_tracker_uri' => 'https://github.com/rkerberos/rkerberos/issues',
27
+ 'changelog_uri' => 'https://github.com/rkerberos/rkerberos/blob/main/CHANGES.md',
28
+ 'documentation_uri' => 'https://github.com/rkerberos/rkerberos/wiki',
29
+ 'source_code_uri' => 'https://github.com/rkerberos/rkerberos',
30
+ 'wiki_uri' => 'https://github.com/rkerberos/rkerberos/wiki',
31
+ 'github_repo' => 'https://github.com/djberg96/rkerberos',
32
+ 'funding_uri' => 'https://github.com/sponsors/rkerberos',
33
+ 'rubygems_mfa_required' => 'true'
34
+ }
26
35
  end
data/spec/config_spec.rb CHANGED
@@ -104,8 +104,8 @@ RSpec.describe Kerberos::Kadm5::Config do
104
104
  it 'responds to max_life' do
105
105
  expect(config).to respond_to(:max_life)
106
106
  end
107
- it 'returns an Integer or nil' do
108
- expect([Integer, NilClass]).to include(config.max_life.class)
107
+ it 'returns an Integer' do
108
+ expect(config.max_life).to be_a(Integer)
109
109
  end
110
110
  end
111
111
 
@@ -113,8 +113,8 @@ RSpec.describe Kerberos::Kadm5::Config do
113
113
  it 'responds to max_rlife' do
114
114
  expect(config).to respond_to(:max_rlife)
115
115
  end
116
- it 'returns an Integer or nil' do
117
- expect([Integer, NilClass]).to include(config.max_rlife.class)
116
+ it 'returns an Integer' do
117
+ expect(config.max_rlife).to be_a(Integer)
118
118
  end
119
119
  end
120
120
 
data/spec/context_spec.rb CHANGED
@@ -18,6 +18,42 @@ RSpec.describe Kerberos::Krb5::Context do
18
18
  end
19
19
  end
20
20
 
21
+ describe 'constructor options' do
22
+ it 'accepts secure: true to use a secure context' do
23
+ expect { described_class.new(secure: true) }.not_to raise_error
24
+ end
25
+
26
+ it 'accepts a profile path via :profile' do
27
+ profile_path = ENV['KRB5_CONFIG'] || '/etc/krb5.conf'
28
+ expect(File).to exist(profile_path)
29
+ expect { described_class.new(profile: profile_path) }.not_to raise_error
30
+ end
31
+
32
+ it 'validates profile argument type' do
33
+ expect { described_class.new(profile: 123) }.to raise_error(TypeError)
34
+ end
35
+
36
+ it 'ignores environment when secure: true' do
37
+ begin
38
+ orig = ENV['KRB5_CONFIG']
39
+ ENV['KRB5_CONFIG'] = '/no/such/file'
40
+ expect { described_class.new(secure: true) }.not_to raise_error
41
+ ensure
42
+ ENV['KRB5_CONFIG'] = orig
43
+ end
44
+ end
45
+
46
+ it 'accepts secure: true together with profile' do
47
+ profile_path = ENV['KRB5_CONFIG'] || '/etc/krb5.conf'
48
+ expect(File).to exist(profile_path)
49
+
50
+ ctx = nil
51
+ expect { ctx = described_class.new(secure: true, profile: profile_path) }.not_to raise_error
52
+ expect(ctx).to be_a(described_class)
53
+ expect { ctx.close }.not_to raise_error
54
+ end
55
+ end
56
+
21
57
  after(:each) do
22
58
  context.close
23
59
  end
@@ -83,6 +83,29 @@ RSpec.describe Kerberos::Krb5::CredentialsCache do
83
83
  end
84
84
  end
85
85
 
86
+ describe '#cache_name and #cache_type' do
87
+ it 'returns the ccache name and type' do
88
+ c = described_class.new(princ)
89
+ expect(c).to respond_to(:cache_name)
90
+ expect(c).to respond_to(:cache_type)
91
+
92
+ expect(c.cache_name).to be_a(String)
93
+ expect(c.cache_type).to be_a(String)
94
+
95
+ # cache_name returns the residual portion of the cache name; default_name
96
+ # may include the type prefix (e.g. "FILE:"). ensure the suffix matches.
97
+ expect(c.cache_name).to eq(c.default_name.split(':').last)
98
+ end
99
+ end
100
+
101
+ describe '#principal' do
102
+ it 'is an alias for primary_principal' do
103
+ c = described_class.new(princ)
104
+ expect(c).to respond_to(:principal)
105
+ expect(c.principal).to eq(c.primary_principal)
106
+ end
107
+ end
108
+
86
109
  describe '#primary_principal' do
87
110
  it 'responds to primary_principal' do
88
111
  c = described_class.new(princ)
@@ -126,4 +149,34 @@ RSpec.describe Kerberos::Krb5::CredentialsCache do
126
149
  expect { c.destroy(true) }.to raise_error(ArgumentError)
127
150
  end
128
151
  end
152
+
153
+ describe '#dup' do
154
+ it 'returns a new cache object with the same properties' do
155
+ c = described_class.new(princ)
156
+ c2 = c.dup
157
+ expect(c2).to be_a(described_class)
158
+ expect(c2.default_name).to eq(c.default_name)
159
+ expect(c2.primary_principal).to eq(c.primary_principal)
160
+ end
161
+
162
+ it 'closing original does not affect duplicate' do
163
+ c = described_class.new(princ)
164
+ c2 = c.dup
165
+ c.close
166
+ expect { c2.default_name }.not_to raise_error
167
+ end
168
+
169
+ it 'closing duplicate does not affect original' do
170
+ c = described_class.new(princ)
171
+ c2 = c.dup
172
+ c2.close
173
+ expect { c.default_name }.not_to raise_error
174
+ end
175
+
176
+ it 'raises when duping closed cache' do
177
+ c = described_class.new(princ)
178
+ c.close
179
+ expect { c.dup }.to raise_error(Kerberos::Krb5::Exception)
180
+ end
181
+ end
129
182
  end
data/spec/kadm5_spec.rb CHANGED
@@ -58,5 +58,44 @@ RSpec.describe Kerberos::Kadm5 do
58
58
  end
59
59
  end
60
60
 
61
- # ... (Due to length, only a representative subset of tests is shown. The rest should be ported similarly.)
61
+ describe '#get_privileges' do
62
+ before(:each) do
63
+ @kadm5 = described_class.new(principal: user, password: pass)
64
+ end
65
+
66
+ after(:each) do
67
+ @kadm5.close
68
+ end
69
+
70
+ it 'returns an integer bitmask by default' do
71
+ result = @kadm5.get_privileges
72
+ expect(result).to be_a(Integer)
73
+ expect(result).not_to eq(0)
74
+ end
75
+
76
+ it 'returns an array of strings when passed a truthy argument' do
77
+ result = @kadm5.get_privileges(true)
78
+ expect(result).to be_a(Array)
79
+ expect(result).not_to be_empty
80
+ expect(result).to all(be_a(String))
81
+ end
82
+
83
+ it 'only contains valid privilege names' do
84
+ result = @kadm5.get_privileges(true)
85
+ valid = %w[GET ADD MODIFY DELETE]
86
+ result.each do |priv|
87
+ expect(valid).to include(priv)
88
+ end
89
+ end
90
+
91
+ it 'does not contain UNKNOWN entries' do
92
+ result = @kadm5.get_privileges(true)
93
+ expect(result).not_to include('UNKNOWN')
94
+ end
95
+
96
+ it 'includes GET for an admin principal' do
97
+ result = @kadm5.get_privileges(true)
98
+ expect(result).to include('GET')
99
+ end
100
+ end
62
101
  end