The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

package DBIx::Class::PassphraseColumn;
BEGIN {
  $DBIx::Class::PassphraseColumn::AUTHORITY = 'cpan:FLORA';
}
BEGIN {
  $DBIx::Class::PassphraseColumn::VERSION = '0.02';
}
# ABSTRACT: Automatically hash password/passphrase columns

use Class::Load 'load_class';
use Sub::Name 'subname';
use namespace::clean;

use parent 'DBIx::Class';


__PACKAGE__->load_components(qw(InflateColumn::Authen::Passphrase));

__PACKAGE__->mk_classdata('_passphrase_columns');


sub register_column {
    my ($self, $column, $info, @rest) = @_;

    if (my $encoding = $info->{passphrase}) {
        $info->{inflate_passphrase} = $encoding;

        $self->throw_exception(q['passphrase_class' is a required argument])
            unless exists $info->{passphrase_class}
                && defined $info->{passphrase_class};

        my $class = 'Authen::Passphrase::' . $info->{passphrase_class};
        load_class $class;

        my $args = $info->{passphrase_args} || {};
        $self->throw_exception(q['passphrase_args' must be a hash reference])
            unless ref $args eq 'HASH';

        my $encoder = sub {
            my ($val) = @_;
            $class->new(%{ $args }, passphrase => $val)->${\"as_${encoding}"};
        };

        $self->_passphrase_columns({
            %{ $self->_passphrase_columns || {} },
            $column => $encoder,
        });

        if (defined(my $meth = $info->{passphrase_check_method})) {
            my $checker = sub {
                my ($row, $val) = @_;
                return $row->get_inflated_column($column)->match($val);
            };

            my $name = join q[::] => $self->result_class, $meth;

            {
                no strict 'refs';
                *$name = subname $name => $checker;
            }
        }
    }

    $self->next::method($column, $info, @rest);
}


sub set_column {
    my ($self, $col, $val, @rest) = @_;

    my $ppr_cols = $self->_passphrase_columns;
    return $self->next::method($col, $ppr_cols->{$col}->($val), @rest)
        if exists $ppr_cols->{$col};

    return $self->next::method($col, $val, @rest);
}


sub new {
    my ($self, $attr, @rest) = @_;

    my $ppr_cols = $self->_passphrase_columns;
    for my $col (keys %{ $ppr_cols }) {
        next unless exists $attr->{$col} && !ref $attr->{$col};
        $attr->{$col} = $ppr_cols->{$col}->( $attr->{$col} );
    }

    return $self->next::method($attr, @rest);
}


1;

__END__
=pod

=encoding utf-8

=head1 NAME

DBIx::Class::PassphraseColumn - Automatically hash password/passphrase columns

=head1 SYNOPSIS

    __PACKAGE__->load_components(qw(PassphraseColumn));

    __PACKAGE__->add_columns(
        id => {
            data_type         => 'integer',
            is_auto_increment => 1,
        },
        passphrase => {
            data_type        => 'text',
            passphrase       => 'rfc2307',
            passphrase_class => 'SaltedDigest',
            passphrase_args  => {
                algorithm   => 'SHA-1',
                salt_random => 20,
            },
            passphrase_check_method => 'check_passphrase',
        },
    );

    __PACKAGE__->set_primary_key('id');

In application code:

    # 'plain' will automatically be hashed using the specified passphrase_class
    # and passphrase_args. The result of the hashing will stored in the
    # specified encoding
    $rs->create({ passphrase => 'plain' });

    my $row = $rs->find({ id => $id });
    my $passphrase = $row->passphrase; # an Authen::Passphrase instance

    if ($row->check_passphrase($input)) { ...

    $row->passphrase('new passphrase');
    $row->passphrase( Authen::Passphrase::RejectAll->new );

=head1 DESCRIPTION

This component can be used to automatically hash password columns using any
scheme supported by L<Authen::Passphrase> whenever the value of these columns is
changed.

=head1 METHODS

=head2 register_column

Chains with the C<register_column> method in C<DBIx::Class::Row>, and sets up
passphrase columns according to the options documented above. This would not
normally be directly called by end users.

=head2 set_column

Hash a passphrase column whenever it is set.

=head2 new

Hash all passphrase columns on C<new()> so that C<copy()>, C<create()>, and
others B<DWIM>.

=head1 COMPARISON TO SIMILAR MODULES

This module is similar to both L<DBIx::Class::EncodedColumn> and
L<DBIx::Class::DigestColumns>. Here's a brief comparison that might help you
decide which one to choose.

=over 4

=item * C<DigestColumns> performs the hashing operation on C<insert> and
C<update>. C<PassphraseColumn> and C<EncodedColumn> perform the operation when
the value is set, or on C<new>.

=item * C<DigestColumns> supports only algorithms of the Digest family.

=item * C<EncodedColumn> employs a set of thin wrappers around different cipher
modules to provide support for any cipher you wish to use and wrappers are very
simple to write.

=item * C<PassphraseColumn> delegates password hashing and encoding to
C<Authen::Passphrase>, which already has support for a huge number of hashing
schemes. Writing a new C<Authen::Passphrase> subclass to support other schemes
is easy.

=item * C<EncodedColumn> and C<DigestColumns> require all values in a hashed column to
use the same hashing scheme. C<PassphraseColumn> stores both the hashed
passphrase value I<and> the scheme used to hash it. Therefore it's possible to
have different rows using different hashing schemes.

This is especially useful when, for example, being tasked with importing records
(e.g. users) from a legacy application, that used a certain hashing scheme and
has no plain-text passwords available, into another application that uses
another hashing scheme.

=item * C<PassphraseColumn> and C<EncodedColumn> support having more than one hashed
column per table and each column can use a different hashing
scheme. C<DigestColumns> is limited to one hashed column per table.

=item * C<DigestColumns> supports changing certain options at runtime, as well as the
option to not automatically hash values on set. Neither C<PassphraseColumn> nor
C<EncodedColumn> support this.

=back

=head1 OPTIONS

This module provides the following options for C<add_column>:

=over 4

=item C<< passphrase => $encoding >>

This specifies the encoding passphrases will be stored in. Possible values are
C<rfc2307> and C<crypt>. The value of C<$encoding> is pass on unmodified to the
C<inflate_passphrase> option provided by
L<DBIx::Class::InflateColumn::Authen::Passphrase>. Please refer to its
documentation for details.

=item C<< passphrase_class => $name >>

When receiving a plain string value for a passphrase, that value will be hashed
using the C<Authen::Passphrase> subclass specified by C<$name>. A value of
C<SaltedDigest>, for example, will cause passphrases to be hashed using
C<Authen::Passphrase::SaltedDigest>.

=item C<< passphrase_args => \%args >>

When attempting to hash a given passphrase, the C<%args> specified in this
options will be passed to the constructor of the C<Authen::Passphrase> class
specified using C<passphrase_class>, in addition to the actual password to hash.

=item C<< passphrase_check_method => $method_name >>

If this option is specified, a method with the name C<$method_name> will be
created in the result class. This method takes one argument, a plain text
passphrase, and returns a true value if the provided passphrase matches the
encoded passphrase stored in the row it's being called on.

=back

=head1 SEE ALSO

L<DBIx::Class::InflateColumn::Authen::Passphrase>

L<DBIx::Class::EncodedColumn>

L<DBIx::Class::DigestColumns>

=head1 AUTHOR

Florian Ragwitz <rafl@debian.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Florian Ragwitz.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut