use warnings;
use strict;
package Net::OAuth2::Scheme::Mixin::VTable;
BEGIN {
$Net::OAuth2::Scheme::Mixin::VTable::VERSION = '0.03';
}
# ABSTRACT: the 'vtable', 'vtable_cache', and 'vtable_pull_queue' option groups
use Net::OAuth2::Scheme::Option::Defines;
# parse_token
# @token -> validator_id, @payload
# vtable_lookup
# validator_id -> @validator
# validate
# @validator, @payload -> valid?, issue_time, expires_in, scope, client_id
# @token = (token_string [, param => value]*)
# @validator = (@secrets [, expiration, scope, ext])
# Token scheme determines format of @secrets, @payload and @validator
# Expiration+@scope may live in either @payload or @validator
#
# What needs to be communicated/shared privately
# between Authorization and Resource servers
# RS -> AS: server_id
# AS -> RS: vtable == (secret_id -> expiration, @data) hash/map
# INTERFACE vtable
# DEFINES
# (AS) vtable_insert
# (RS) vtable_lookup
Define_Group vtable => 'shared_cache',
qw(vtable_insert vtable_lookup);
# IMPLEMENTATION vtable_shared_cache FOR vtable
# SUMMARY
# secure shared cache, everybody can read/write
# REQUIRES
# vtable_cache
# NOTES
# vtable_insert = vtable_put
# vtable_lookup = vtable_get
# this works for
# (*) AS and RS being the same process
# (*) AS and RS being on the same host
# cache is file-based or shared-memory cache
# (*) AS and RS are on the same server farm
# cache is memcached
sub pkg_vtable_shared_cache {
my __PACKAGE__ $self = shift;
if ($self->is_auth_server) {
$self->install( vtable_insert => $self->uses('vtable_put'));
}
if ($self->is_resource_server) {
$self->install( vtable_lookup => $self->uses('vtable_get'));
}
return $self;
}
# IMPLEMENTATION vtable_authserv_push FOR vtable
# SUMMARY
# RS-local cache with AS pushing each new entry
# REQUIRES
# vtable_cache (+ (RS) vtable_put)
# vtable_push(entry) => send entry
# push handler => recv entry, vtable_pushed(entry)
# EXPORTS
# vtable_pushed (for push handler)
# NOTES
# vtable_insert = vtable_push
# vtable_lookup = vtable_get
# vtable_pushed = vtable_put
sub pkg_vtable_authserv_push {
my __PACKAGE__ $self = shift;
if ($self->is_auth_server) {
$self->install( vtable_insert => $self->uses('vtable_push'));
}
if ($self->is_resource_server) {
$self->install( vtable_lookup => $self->uses('vtable_get'));
$self->install( vtable_pushed => $self->uses('vtable_put'));
$self->export('vtable_pushed');
}
return $self;
}
# IMPLEMENTATION vtable_resource_pull FOR vtable
# SUMMARY
# RS-local cache with RS pulling on cache miss
# REQUIRES
# vtable_cache
# vtable_pull_queue
# vtable_pull => send query, return response
# pull handler => recv query, respond vtable_dump(query)
# EXPORTS
# vtable_dump (for pull handler)
# NOTES
# vtable_insert = vtable_enqueue
# vtable_lookup is
# vtable_get or
# (vtable_load(vtable_pull(vtable_query)) and
# retry vtable_get)
sub pkg_vtable_resource_pull {
my __PACKAGE__ $self = shift;
if ($self->is_auth_server) {
$self->install( vtable_insert => $self->uses('vtable_enqueue'));
$self->export('vtable_dump');
}
if ($self->is_resource_server) {
my ( $vtable_get, $vtable_load, $vtable_query, $vtable_pull) = $self->uses_all
(qw(vtable_get vtable_load vtable_query vtable_pull));
$self->install( vtable_lookup => sub {
my $v_id = shift;
my ($error, @found) = $vtable_get->($v_id);
unless ($error || @found) {
($error) = $vtable_load->($vtable_pull->($vtable_query->()));
return $error if $error;
($error,@found) = $vtable_get->($v_id);
}
return ($error, @found);
});
}
return $self;
}
# INTERFACE vtable_cache
# DEFINES
# vtable_put : id,expiration,@stuff ->;
# vtable_get : id -> expiration,@stuff
Define_Group vtable_cache => 'object',
qw(vtable_put vtable_get);
Default_Value cache_grace => 300;
Default_Value cache_prefix => 'vtab';
# vtable_cache_object
# IMPLEMENTS vtable_cache
# REQUIRES
# cache => Cache::Memory, Cache::File, or Cache::Memcached object
# OPTIONS
# cache_grace => number;
# delay cache expiration by this many seconds (default = 300)
# cache_prefix => string
# keys are prefixed with this (default = 'vtab:')
sub pkg_vtable_cache_object {
my __PACKAGE__ $self = shift;
my ( $cache, $grace, $prefix) = $self->uses_all
(qw(cache cache_grace cache_prefix));
$prefix .= ':' if length($prefix) && $prefix !~ m/:\z/;
$self->croak("cache_prefix ($prefix) cannot contain interior colon (:)")
if length($prefix) && $prefix =~ m{:[^:]};
$self->install( vtable_get => sub {
my $v = $cache->thaw($prefix . $_[0]);
return (undef, @{defined($v) ? $v : []});
});
$self->install( vtable_put => sub {
my $id = shift;
$cache->freeze($prefix . $id, [@_], $_[0] + $grace);
return ();
});
}
# INTERFACE vtable_pull_queue
# DEFINES
# (AS) vtable_enqueue
# vtable_dump
# (RS) vtable_query
# vtable_load
Define_Group vtable_pull_queue => 'default',
qw(vtable_enqueue
vtable_dump
vtable_query
vtable_load);
# default implementation
# REQUIRES
# (RS) vtable_put
sub pkg_vtable_pull_queue_default {
my __PACKAGE__ $self = shift;
if ($self->is_auth_server) {
my $cache_grace = $self->uses('cache_grace');
my $vpqueue = {};
my $latest = [];
$self->install( vtable_enqueue => sub {
my ($v_id, $expiration, $now) = @_;
# insert maintaining @$latest in order of increasing expiration
my $i = $#{$latest};
--$i while ($i >= 0 && $latest->[$i]->[1] > $expiration);
splice @{$latest}, $i+1, 0, [@_];
# prune expired entries from @$latest
$i = 0;
++$i while ($i < @$latest && $latest->[$i]->[1] + $cache_grace < $now);
splice @{$latest}, 0, $i;
# prune expired entries from each batch of %$vpqueue
for my $entries (values %{$vpqueue}) {
$i = $#{$entries};
--$i while ($i >= 0 && $entries->[$i]->[1] + $cache_grace < $now);
splice @{$entries}, $i+1;
}
# remove empty batches from %$vpqueue
delete @{$vpqueue}{grep {!@{$vpqueue->{$_}}} keys %{$vpqueue}};
# never fails (?)
return ();
});
$self->install( vtable_dump => sub {
my ($now, $last_recv) = @_;
# remove batches whose receipt has been acknowledged
delete @{$vpqueue}{grep {$_ <= $last_recv} keys %$vpqueue};
# insert @$latest entries into vpqueue
if (@$latest) {
unless (exists $vpqueue->{$now}) {
# make a new batch
$vpqueue->{$now} = [reverse @{$latest}];
}
else {
# merge into current batch; this should be rare
# and when it happens @$latest should be short
my $nqueue = $vpqueue->{$now};
for my $e (@{$latest}) {
my $i = 0;
++$i while ($i < @{$nqueue} && $nqueue->[$i]->[1] > $e->[1]);
splice @{$nqueue}, $i, 0, $e;
}
}
$latest = [];
}
# send everything
my @r = ();
push @r, @$_ for values %{$vpqueue};
return (undef, $now, \@r);
});
}
if ($self->is_resource_server) {
my $vtable_put = $self->uses('vtable_put');
my $last_recv;
$self->install( vtable_query => sub {
my $now = time();
return ($now, (defined($last_recv) && $now == $last_recv
? $last_recv - 1 : $last_recv));
});
$self->install( vtable_load => sub {
my ($error, $now, $recvd) = @_;
return ($error) if $error;
for my $entry (@$recvd) {
$vtable_put->(@$entry);
}
$last_recv = $now;
return (undef, scalar(@$recvd));
});
}
return $self;
}
1;
__END__
=pod
=head1 NAME
Net::OAuth2::Scheme::Mixin::VTable - the 'vtable', 'vtable_cache', and 'vtable_pull_queue' option groups
=head1 VERSION
version 0.03
=head1 DESCRIPTION
This is an internal module that implements the abstract shared cache
for sharing secrets between authorization servers and resource
servers.
See L<Net::OAuth2::Scheme::Factory> for actual option usage.
=head1 AUTHOR
Roger Crew <crew@cs.stanford.edu>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2012 by Roger Crew.
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