File Coverage

blib/lib/Role/Kerberos.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             package Role::Kerberos;
2              
3 1     1   21615 use 5.010;
  1         4  
4 1     1   5 use strict;
  1         4  
  1         25  
5 1     1   5 use warnings FATAL => 'all';
  1         12  
  1         43  
6              
7 1     1   893 use Moo::Role;
  1         22331  
  1         7  
8             #use namespace::clean;
9              
10 1     1   819 use Authen::Krb5 ();
  0            
  0            
11             use Scalar::Util ();
12             use Carp ();
13             #use Try::Tiny ();
14              
15             # Authen::Krb5 contains a global, presumably non-threadsafe pointer to
16             # this execution context. This is the best way I can muster dealing
17             # with it.
18              
19             BEGIN {
20             Authen::Krb5::init_context();
21             }
22              
23             END {
24             Authen::Krb5::free_context();
25             }
26              
27             sub _is_really {
28             my ($x, $class) = @_;
29             defined $x and ref $x and Scalar::Util::blessed($x) and $x->isa($class);
30             }
31              
32             sub _k5err {
33             Carp::croak(@_, ': ', Authen::Krb5::error());
34             }
35              
36             =head1 NAME
37              
38             Role::Kerberos - A role for managing Kerberos 5 credentials
39              
40             =head1 VERSION
41              
42             Version 0.01_03
43              
44             =cut
45              
46             our $VERSION = '0.01_03';
47              
48             =head1 SYNOPSIS
49              
50             package My::Kerbject;
51              
52             use Moo;
53             with 'Role::Kerberos';
54              
55             has other_stuff => (
56             # ...
57             );
58              
59             # go nuts...
60              
61             # ...elsewhere:
62              
63             package Somewhere::Else;
64              
65             my $krb = My::Kerbject->new(
66             principal => 'robot@ELITE.REALM',
67             keytab => '/etc/robot/creds.keytab',
68             ccache => '/var/lib/robot/krb5cc',
69             other_stuff => 'derp',
70             );
71              
72             =head1 DESCRIPTION
73              
74             L is kind of unwieldy. L is too
75             simple (no keytabs). L requires too much
76             effort (can't specify keytabs/ccaches outside of environment
77             variables) and L hasn't been touched in 13 years.
78              
79             The purpose of this module is to enable you to strap onto an existing
80             L(L) object the functionality necessary to acquire and
81             maintain a Kerberos TGT. My own impetus for writing this module
82             involves making connections authenticated via L and
83             GSSAPI where the keys come from a keytab in a non-default location and
84             the consistency of C<%ENV> is not reliable (that is, in a Web app).
85              
86             =head1 METHODS
87              
88             =head2 new %PARAMS
89              
90             As with all roles, these parameters get integrated into your class's
91             constructor, and also serve as accessor methods. Every one is
92             read-only, and every one is optional except L.
93              
94             =over 4
95              
96             =item realm
97              
98             The default realm. Taken from the default principal, or otherwise the
99             system default realm if not defined.
100              
101             =cut
102              
103             around BUILDARGS => sub {
104             my $orig = shift;
105             my $class = shift;
106             my %p;
107             if (@_ and ref $_[0] eq 'HASH') {
108             %p = %{$_[0]};
109             }
110             else {
111             %p = @_;
112             }
113              
114             Carp::croak('Must supply at least a principal')
115             unless defined $p{principal} and $p{principal} ne '';
116              
117             if ($p{principal} =~ /@/) {
118             $p{principal} = _coerce_principal($p{principal});
119             $p{realm} ||= $p{principal}->realm;
120             }
121             else {
122             $p{realm} ||= Authen::Krb5::get_default_realm();
123             $p{principal} = sprintf '%s@%s', @p{qw(principal realm)};
124             }
125              
126             $orig->($class, %p);
127             };
128              
129             has realm => (
130             is => 'rw',
131             lazy => 1,
132             default => sub { Authen::Krb5::get_default_realm(); },
133             );
134              
135             =item principal
136              
137             The default principal. Can (should) also contain a realm. If a realm
138             is missing from the principal, it will be added from
139             L. Coerced from a string into a
140             L object. B.
141              
142             =cut
143              
144             sub _coerce_principal {
145             my $n = shift;
146             return $n if _is_really($n, 'Authen::Krb5::Principal');
147              
148             my $r = shift || Authen::Krb5::get_default_realm();
149              
150             $n = sprintf '%s@%s', $n, $r unless $n =~ /@/;
151              
152             Authen::Krb5::parse_name($n)
153             or _k5err("Could not resolve principal $n");
154             }
155              
156             has principal => (
157             is => 'ro',
158             isa => sub { _is_really(shift, 'Authen::Krb5::Principal') },
159             required => 1,
160             trigger => sub { $_[0]->realm($_[0]->principal->realm) },
161             coerce => \&_coerce_principal,
162             );
163              
164             =item keytab
165              
166             A keytab, if other than C<$ENV{KRB5_KTNAME}>. Will default to that or
167             the system default (e.g. C). Coerced from a file
168             path into an L object.
169              
170             =cut
171              
172             sub _coerce_kt {
173             my $val = shift;
174             return $val if _is_really($val, 'Authen::Krb5::Keytab');
175              
176             $val = "FILE:$val" unless $val =~ /^[^:]+:/;
177              
178             Authen::Krb5::kt_resolve($val) or _k5err("Could not load keytab $val");
179             }
180              
181             has keytab => (
182             is => 'ro',
183             isa => sub { _is_really(shift, 'Authen::Krb5::Keytab') },
184             lazy => 1,
185             coerce => \&_coerce_kt,
186             default => sub {
187             Authen::Krb5::kt_default() or _k5err("Could not load default keytab");
188             },
189             );
190              
191             =item ccache
192              
193             The locator (e.g. file path) of a credential cache, if different from
194             C<$ENV{KRB5CCNAME}> or the system default. Coerced into an
195             L object.
196              
197             =cut
198              
199             has ccache => (
200             is => 'ro',
201             lazy => 1,
202             coerce => sub {
203             my $val = shift;
204             return $val if _is_really($val, 'Authen::Krb5::Ccache');
205              
206             $val = "FILE:$val" unless $val =~ /^FILE:/i;
207              
208             my $kt = Authen::Krb5::cc_resolve($val)
209             or _k5err("Could not load credential cache $val");
210             },
211             default => sub {
212             Authen::Krb5::cc_default()
213             or _k5err("Could not resolve default credentials cache");
214             },
215             );
216              
217             =back
218              
219             =head2 kinit %PARAMS
220              
221             Log in to Kerberos. Parameters are optional.
222              
223             =over 4
224              
225             =item principal
226              
227             The principal, if different from that in the constructor.
228              
229             =item realm
230              
231             The realm, if different from that in the constructor. Ignored if the
232             principal contains a realm.
233              
234             =item password
235              
236             The Kerberos password, if logging in with a password. (See
237             L for a handy way of ingesting a password from the
238             command line.)
239              
240             =item keytab
241              
242             A keytab, if different from that in the constructor or
243             C<$ENV{KRB5_KTNAME}>. Will be coerced from a file name.
244              
245             =item service
246              
247             A service principal, if different from C.
248              
249             =back
250              
251             =cut
252              
253             sub kinit {
254             my $self = shift;
255             my %p = @_;
256              
257             $p{realm} ||= $self->realm;
258             $p{principal} = $p{principal}
259             ? _coerce_principal(@p{qw(principal realm)}) : $self->principal;
260              
261             my $tgt;
262             if (defined $p{password}) {
263             my @a = @p{qw(principal password)};
264             push @a, $p{service} if defined $p{service};
265              
266             $tgt = Authen::Krb5::get_init_creds_password(@a)
267             or _k5err('Failed to get TGT');
268             }
269             else {
270             $p{keytab} = $p{keytab} ? _coerce_kt($p{keytab}) : $self->keytab;
271             my @a = @p{qw(principal keytab)};
272             push @a, $p{service} if defined $p{service};
273              
274             $tgt = Authen::Krb5::get_init_creds_keytab(@a)
275             or _k5err('Failed to get TGT');
276             }
277              
278             my $cc = $self->ccache;
279             $cc->initialize($p{principal});
280             $cc->store_cred($tgt);
281             }
282              
283             =head2 klist %PARAMS
284              
285             =cut
286              
287             sub klist {
288             my $self = shift;
289              
290             my $cc = $self->ccache;
291             #my $p = $self->principal;
292             my @out;
293             if (my $cursor = $cc->start_seq_get) {
294             while (my $cred = $cc->next_cred($cursor)) {
295             push @out, {
296             principal => $cred->client,
297             service => $cred->server,
298             auth => $cred->authtime,
299             start => $cred->starttime,
300             end => $cred->endtime,
301             renew => $cred->renew_till,
302             ticket => $cred->ticket,
303             # this segfaults when Authen::Krb5::Keyblock->DESTROY
304             # is called with the key content memory out of bounds,
305             # keyblock => $cred->keyblock,
306             };
307             }
308             $cc->end_seq_get($cursor);
309             }
310              
311             return unless @out;
312             wantarray ? @out : \@out;
313             }
314              
315             =head2 kexpired
316              
317             Returns true if any tickets in the cache are expired.
318              
319             =cut
320              
321             sub kexpired {
322             my $self = shift;
323             my $now = time;
324              
325             return scalar grep { $_->{end} < $now } $self->tickets;
326             }
327              
328              
329             # wishful thinking: Authen::Krb5 does not at the moment expose either
330             # ticket flags or krb5_get_renewed_creds.
331              
332             # =head2 krenew
333              
334             # Checks the TGT and reauthenticates if expired. This is I
335              
336             # =cut
337              
338             =head2 kdestroy
339              
340             Destroy the credentials cache (if there is something to destroy).
341              
342             =cut
343              
344             sub kdestroy {
345             my $self = shift;
346             $self->ccache->destroy if $self->klist;
347             }
348              
349             # XXX do we actually want this to happen?
350             # sub DEMOLISH {
351             # $_[0]->kdestroy;
352             # }
353              
354             # XXX more sensible?
355             sub DEMOLISH {
356             my $self = shift;
357             for my $entry ($self->klist) {
358             delete $entry->{keyblock};
359             }
360             }
361              
362             =head1 AUTHOR
363              
364             Dorian Taylor, C<< >>
365              
366             =head1 SEE ALSO
367              
368             =over 4
369              
370             =item L
371              
372             =item L
373              
374             =back
375              
376              
377             =head1 BUGS
378              
379             Please report any bugs or feature requests to C
380             rt.cpan.org>, or through the web interface at
381             L. I
382             will be notified, and then you'll automatically be notified of
383             progress on your bug as I make changes.
384              
385             =head1 LICENSE AND COPYRIGHT
386              
387             Copyright 2015 Dorian Taylor.
388              
389             Licensed under the Apache License, Version 2.0 (the "License"); you
390             may not use this file except in compliance with the License. You may
391             obtain a copy of the License at
392             L.
393              
394             Unless required by applicable law or agreed to in writing, software
395             distributed under the License is distributed on an "AS IS" BASIS,
396             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
397             implied. See the License for the specific language governing
398             permissions and limitations under the License.
399              
400             =cut
401              
402             1; # End of Role::Kerberos