Skip to content

Instantly share code, notes, and snippets.

@nerdstrike
Last active January 11, 2019 15:41
Show Gist options
  • Save nerdstrike/55634af73bb9054af699dc11eaa985a4 to your computer and use it in GitHub Desktop.
Save nerdstrike/55634af73bb9054af699dc11eaa985a4 to your computer and use it in GitHub Desktop.
Why no authenticate?
package Catalyst::Authentication::Store::ElasticSearch;
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use Catalyst::Exception;
use Catalyst::Authentication::Store::ElasticSearch::User; # The default user storage class
has user_class => ( is => 'ro', isa => 'Str', default => 'Catalyst::Authentication::Store::ElasticSearch::User');
has config => (is => 'rw');
# Catalyst::Authentication modules must accept non-moosey arguments
# 1. $config, a hashref containing the server config
# 2. $app, sometimes $c, the plack app itself
# 3. $realm, a regime to enforce particular security constraints
around BUILDARGS => sub {
my ($call, $class, $config, $app, $realm) = @_;
return $class->$call(
config => $config
); # In case we should need the various options later
};
=head2 from_session
Get me a user hashref with a session ID
=cut
sub from_session {
my ($self, $c, $frozenuser) = @_;
return $self->user_class->from_session($frozenuser);
}
=head2 for_session
Create a session for this user
=cut
sub for_session {
my ($self, $c, $user) = @_;
return $user->for_session();
}
=head2 find_user
Provide authentication and a request context to get a user hashref back
=cut
sub find_user {
my ($self, $authinfo, $c) = @_;
use Data::Dumper;
print Dumper $c; # This returns the application with a request and server params
my $user = $c->model('ElasticSearch')->authenticate_user($authinfo);
return $self->user_class->load($user);
}
=head2 user_supports
The user class supports particular functionality, e.g. sessions.
For consumption by the web framework
=cut
sub user_supports {
my $self = shift;
# this must be a class method on the user_class
return $self->user_class->supports( @_ );
}
__PACKAGE__->meta->make_immutable;
1;
package Catalyst::Model::ElasticSearch;
use Moose;
use namespace::autoclean;
use Search::Elasticsearch;
use Registry::Utils::File qw/slurp_file/;
use Registry;
use Carp;
use JSON;
extends 'Catalyst::Model';
has 'nodes' => (
is => 'rw',
lazy => 1,
default => "localhost:9200",
);
=head2 transport
The transport to use to interact with the Elasticsearch API. See L<Search::Elasticsearch::Transport|Search::Elasticsearch::Transport> for options.
=cut
has 'transport' => (
is => 'rw',
lazy => 1,
default => "+Search::Elasticsearch::Transport",
);
=head2 _additional_opts
Stores other key/value pairs to pass to L<Search::Elasticsearch|Search::Elasticsearch>.
=cut
has '_additional_opts' => (
is => 'rw',
lazy => 1,
isa => 'HashRef',
default => sub { { send_get_body_as => 'POST', cxn_pool => 'Static'} },
);
=head2 _es
The L<Search::Elasticsearch> object.
The follwing helper methods have been replaced by the Search::Elasticsearch::Bulk
class. Similarly, scrolled_search() has been replaced by the Search::Elasticsearch::Scroll.
These helper classes are accessible as:
$bulk = $e->bulk_helper( %args_to_new );
$scroll = $e->scroll_helper( %args_to_new );
==>
- remove bulk_(index|create|delete) and reindex
- add bulk_helper, scroll_helper
- remove searchqs, scrolled_search (not supported)
- add indices (returns Search::Elasticsearch::Client::Indices
- add cluster (returns Search::Elasticsearch::Client::Cluster)
- other?
Given the method returns a Search::Elasticsearch::Client::Direct it's better
to look at what it now supports.
See https://metacpan.org/pod/Search::Elasticsearch::Client::Direct for a list of methods
grouped according to category
=cut
has '_es' => (
is => 'ro',
lazy => 1,
required => 1,
builder => '_build_es',
handles => {
map { $_ => $_ }
qw(
search scrolled_search count index get get_source mget create delete
bulk bulk_helper scroll_helper indices
)
},
);
sub _build_es {
my $self = shift;
return Search::Elasticsearch->new(
nodes => $self->nodes,
transport => $self->transport,
%{ $self->_additional_opts },
);
}
around BUILDARGS => sub {
my $orig = shift;
my $class = shift;
my $params = $class->$orig(@_);
# NOTE: also update this: other stuff deprecated?
# See https://metacpan.org/pod/Search::Elasticsearch#MIGRATING-FROM-ElasticSearch.pm
if (defined $params->{servers}) {
carp "Passing 'servers' is deprecated, use 'nodes' now";
$params->{nodes} = delete $params->{servers};
}
my %additional_opts = %{$params};
delete $additional_opts{$_} for qw/ nodes transport /;
$params->{_additional_opts} = \%additional_opts;
return $params;
};
# Automatically deploy schemas to the configured backend if it is required
around _build_es => sub {
my $orig = shift;
my $self = shift;
my $client = $self->$orig(@_);
foreach my $schema_name (qw/authentication reports trackhub/) {
my $schema_path = File::Spec->catfile(
$self->config->{schema_location}, # Defined in Registry.pm
$schema_name.'_mappings.json'
);
# Alias user file name to its schema name. These should have been the same
my $index_name = $schema_name;
if ($schema_name eq 'authentication') {
$index_name = 'users';
}
print "Creating index '$schema_name' with mapping $schema_path\n";
print "App location = ".$self->config->{wtf}."\n";
# Create indexes and load mappings if they're not present
unless ($client->indices->exists( index => $schema_name ) ) {
$client->indices->create(
index => $index_name.'_v1',
# FIXME, this should reflect actual schema version, but is deeply
# embedded into deployment
body => decode_json( slurp_file( $schema_path ) )
);
}
}
return $client;
};
=head2 authenticate_user
The anonymous user has provided authentication credentials. Fetch their
profile from the user index, constrained on their credentials.
=cut
sub authenticate_user {
my ($self, $authinfo) = @_;
my %query = ( bool => { must => [] } );
# TODO: Constrain to specific set of keys and nothing else to prevent
# improper login attempts
foreach my $key (keys %{$authinfo}) {
push @{ $query{bool}{must} }, { term => { $key => $authinfo->{$key} } };
}
%query = $self->_decorate_query(%query);
my $response = $self->_es->search(%query);
if ($response->{hits}{total} == 1) {
return $response->{hits}{hits}[0];
} else {
return;
}
}
__PACKAGE__->meta->make_immutable;
1;
package Catalyst::Authentication::Store::ElasticSearch::User;
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use Catalyst::Exception;
use Catalyst::Utils;
use LWP;
use JSON;
use Try::Tiny;
use namespace::autoclean;
extends 'Catalyst::Authentication::User';
has supported_features => (is => 'ro', default => sub { { session => 1, roles => 1 } });
has id => (is => 'rw', isa => 'Str'); # Unique ID for user
has roles => (is => 'rw', isa => 'ArrayRef'); # Roles of the user
has raw => (is => 'rw'); # Verbatim decoded copy of JSON from _source field of the result
=head2 load
Takes a single result from a user search in Elasticsearch and unpacks the relevant elements.
Required by Catalyst::Plugin::Authentication
=cut
sub load {
my ($self, $user_result) = @_;
return unless $user_result;
my %thing = JSON::decode_json($user_result);
$self->id( $thing{_id} ); # Delegate unique IDs to the storage engine
$self->roles( $thing{_source}{roles} );
$self->raw( $thing{_source} );
return $self;
}
=head2 get
Return an element of configuration for the user
Required by Catalyst::Plugin::Authentication
=cut
sub get {
my ($self, $field) = @_;
if (exists $self->raw->{$field}) {
return $self->raw->{$field};
}
}
=head2 get_object
Return representation for user
Required by Catalyst::Plugin::Authentication
=cut
sub get_object {
my ($self) = @_;
return $self->raw;
}
=head2 for_session
Returns a serialised user for storage in the session. This is a JSON
representation of the user.
Required by Catalyst::Plugin::Authentication
=cut
sub for_session {
my ($self) = @_;
return JSON::encode_json($self->raw);
}
=head2 from_session
Receives the cached user from the session (produced via for_session), and deserialises.
It also unpacks the interesting bits for easy access
Required by Catalyst::Plugin::Authentication
=cut
sub from_session {
my ($self, $frozen_user) = @_;
my %user = JSON::decode_json($frozen_user);
$self->id( $user{_id});
$self->roles( $user{_source}{roles});
$self->raw($user{_source});
return $self;
}
__PACKAGE__->meta->make_immutable(inline_constructor => 0);
1;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment