The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#+##############################################################################
#                                                                              #
# File: Authen/Credential.pm                                                   #
#                                                                              #
# Description: abstraction of a credential                                     #
#                                                                              #
#-##############################################################################

#
# module definition
#

package Authen::Credential;
use strict;
use warnings;
our $VERSION  = "1.0_1";
our $REVISION = sprintf("%d.%02d", q$Revision: 1.12 $ =~ /(\d+)\.(\d+)/);

#
# used modules
#

use No::Worries::Die qw(dief);
use Params::Validate qw(validate_with validate_pos :types);
use URI::Escape qw(uri_escape uri_unescape);

#
# global variables
#

our(
    $_IdRe,           # regexp matching an identifier
    $_ValChars,       # set of all allowed value characters
    $_SepChars,       # set of all allowed separator characters
    %_LoadedModule,   # hash of successfully loaded modules
    %ValidationSpec,  # per-scheme Params::Validate specification
    %Preparator,      # per-scheme and target preparator code
);

$_IdRe = qr{[a-z][a-z0-9]*};
$_ValChars = q{a-zA-Z0-9/\-\+\_\~\.\:};
$_SepChars = q{\,\ };

#+++############################################################################
#                                                                              #
# helper functions                                                             #
#                                                                              #
#---############################################################################

#
# make sure a module is loaded
#

sub _require ($) {
    my($module) = @_;

    return if $_LoadedModule{$module};
    eval("require $module"); ## no critic 'ProhibitStringyEval'
    if ($@) {
        $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
        dief("failed to load %s: %s", $module, $@);
    } else {
        $_LoadedModule{$module} = 1;
    }
}

#
# check that the data matches the per-scheme specification
#

sub _check ($@) {
    my($scheme);

    $scheme = shift(@_);
    dief("invalid credential scheme (missing validation spec): %s", $scheme)
        unless $ValidationSpec{$scheme};
    return(validate_with(
        params => \@_,
        spec => {
            %{ $ValidationSpec{$scheme} },
            scheme => {
                type    => SCALAR,
                regex   => qr/^\Q$scheme\E$/,
                default => $scheme,
            },
        },
        stack_skip => 2,
    ));
}

#+++############################################################################
#                                                                              #
# object oriented interface                                                    #
#                                                                              #
#---############################################################################

#
# constructors
#

sub new : method {
    my($class, %option, $cc, $scheme);

    $class = shift(@_);
    if ($class eq __PACKAGE__) {
        # toplevel constructor
        %option = validate_with(
            params => \@_,
            spec => {
                scheme => {
                    type    => SCALAR,
                    regex   => $_IdRe,
                    default => "none",
                },
            },
            allow_extra => 1,
        );
        $cc = $class . "::" . $option{scheme};
        _require($cc);
        return($cc->new(\%option));
    }
    # inherited constructor
    $scheme = substr($class, length(__PACKAGE__) + 2);
    return(bless({ _check($scheme, @_) }, $class));
}

sub parse : method {
    my($class, $string, @list, %option);

    $class = shift(@_);
    validate_pos(@_, { type => SCALAR })
        unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
    $string = shift(@_);
    return($class->new()) if $string eq "";
    dief("invalid credential string: %s", $string)
        unless $string =~ /^[${_ValChars}${_SepChars}\%\=]+$/o;
    @list = split(/[${_SepChars}]+/o, $string);
    dief("invalid credential string: %s", $string)
        unless @list and $list[0] =~ /^($_IdRe)$/o;
    %option = (scheme => shift(@list));
    foreach my $kv (@list) {
        if ($kv =~ /^($_IdRe)\=([$_ValChars\%]*)$/o) {
            dief("duplicate credential key: %s", $1)
                if exists($option{$1});
            $option{$1} = uri_unescape($2);
        } else {
            dief("invalid credential key=value: %s", $kv);
        }
    }
    return($class->new(\%option));
}

#
# transformers
#

sub hash : method {
    my($self);

    $self = shift(@_);
    validate_pos(@_) if @_;
    return($self) unless wantarray();
    return(%{ $self });
}

sub string : method {
    my($self, @parts);

    $self = shift(@_);
    validate_pos(@_) if @_;
    dief("invalid credential: no scheme") unless $self->{scheme};
    @parts = ($self->{scheme});
    foreach my $key (sort(keys(%{ $self }))) {
        next if $key eq "scheme";
        push(@parts, $key . "=" . uri_escape($self->{$key}, "^$_ValChars"));
    }
    return(join(" ", @parts));
}

#
# accessors
#

foreach my $name (qw(scheme)) {
    no strict "refs";
    *{ $name } = sub {
        my($self);
        $self = shift(@_);
        validate_pos(@_) if @_;
        return($self->{$name});
    };
}

#
# generic check method using the Params::Validate specification
#

sub check : method {
    my($self);

    $self = shift(@_);
    validate_pos(@_) if @_;
    return(_check($self->scheme(), $self));
}

#
# generic prepare method using the declared preparators
#

sub prepare : method {
    my($self, $target, $preparator);

    $self = shift(@_);
    validate_pos(@_, { type => SCALAR })
        unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
    $target = shift(@_);
    $preparator = $Preparator{$self->scheme()}{$target};
    return($preparator->($self)) if $preparator;
    dief("invalid %s credential preparation target: %s",
         $self->scheme(), $target);
}

1;

__DATA__

=head1 NAME

Authen::Credential - abstraction of a credential

=head1 SYNOPSIS

  use Authen::Credential;
  use Authen::Credential::plain;
  use Getopt::Long qw(GetOptions);
  use Config::General qw(ParseConfig);
  use HTTP::Request;

  # creation
  $cred = Authen::Credential->new(
      scheme => "plain",
      name   => "system",
      pass   => "manager",
  );
  # idem directly using the sub-class
  $cred = Authen::Credential::plain->new(
      name   => "system",
      pass   => "manager",
  );

  # get credential from command line option
  GetOptions(\%Option,
      "auth=s",
      ...
  );
  $cred = Authen::Credential->parse($Option{auth});

  # get credential from configuration file
  %Option = ParseConfig(-ConfigFile => "...");
  $cred = Authen::Credential->new($Option{auth});

  # access the credential attributes
  if ($cred->scheme() eq "plain") {
      printf("user name is %s\n", $cred->name());
  }

  # use the prepare() method to get ready-to-use data
  $req = HTTP::Request->new(GET => $url);
  $req->header(Authorization => $cred->prepare("HTTP.Basic"));

=head1 DESCRIPTION

This module offers abstractions of credentials, i.e. something that
can be used to authenticate. It allows the creation and manipulation of
credentials. In particular, it defines a standard string representation
(so that credentials can be given to external programs as command line
options), a standard structured representation (so that credentials can
be stored in structured configuration files or using JSON) and
"preparators" that can transform credentials into ready-to-use data for
well known targets.

Different authentication schemes (aka credential types) are supported.
This package currently supports C<none>, C<plain> and C<x509> but others
can be added by providing the supporting code in a separate module.

A Python implementation of the same credential abstractions is available
at L<https://github.com/cern-mig/python-auth-credential> so credentials
can be shared between different programming languages.

For a given scheme, a credential is represented by an object with a
fixed set of string attributes. For instance, the C<plain> scheme has
two attributes: C<name> and C<pass>. More information is provided by
the scheme specific module, for instance L<Authen::Credential::plain>.

=head1 STRING REPRESENTATION

The string representation of a credential is made of its scheme
followed by its attributes as key=value pairs, seperated by space.

For instance, for the C<none> scheme with no attributes:

  none

And the the C<plain> scheme with a name and password:

  plain name=system pass=manager

If needed, the characters can be URI-escaped, see L<URI::Escape>. All
non-alphanumerical characters should be escaped to avoid parsing
ambiguities.

The string representation is useful to give a program through its
command line options. For instance:

  myprog --uri http://foo:80 --auth "plain name=system pass=manager"

=head1 STRUCTURED REPRESENTATION

The structured representation of a credential is made of its scheme
and all its attributes as a string table.

Here is for instance how it could end up using JSON:

  {"scheme":"plain","name":"system","pass":"manager"}

The same information could be stored in a configuration file. Here is
an example using the Apache syntax, which is for instance supported by
L<Config::General>:

  <auth>
    scheme = plain
    name   = system
    pass   = manager
  </auth>

=head1 METHODS

This module supports the following methods:

=over

=item new([OPTIONS])

return a new credential object (class method); the OPTIONS are its
attributes

=item parse(STRING)

return a new credential object from its string representation (class
method)

=item hash()

return its structured representation as a reference to a hash

=item string()

return its string representation

=item check()

check that the credential contains the expected attributes

=item prepare(TARGET)

use the credential to prepare data for a given target (this is scheme
specific)

=item scheme()

return the authentication scheme of the credential

=back

In addition, the attributes can be accessed using eponymous methods.
See the example in the L</"SYNOPSIS"> section.

=head1 SEE ALSO

L<Authen::Credential::none>,
L<Authen::Credential::plain>,
L<Authen::Credential::x509>,
L<URI::Escape>,

=head1 AUTHOR

Lionel Cons L<http://cern.ch/lionel.cons>

Copyright (C) CERN 2011-2013