use warnings;
use strict;
package Jifty::API;
=head1 NAME
Jifty::API - Manages and allow reflection on the Jifty::Actions that
make up a Jifty application's API
=head1 SYNOPSIS
# Find the full name of an action
my $class = Jifty->api->qualify('SomeAction');
# New users cannot run some actions
if (Jifty->web->current_user->age < 18) {
Jifty->api->deny(qr/Vote|PurchaseTobacco/);
}
# Some users cannot even see some actions
if (Jifty->web->current_user->id > 10) {
Jifty->api->hide('Foo');
Jifty->api->show('FooBar');
Jifty->api->hide('FooBarDeleteTheWorld');
}
# Fetch the class names of all actions
my @actions = Jifty->api->all_actions;
# Fetch the class names of all the allowed actions
my @allowed = Jifty->api->actions;
# Fetch all of the visible actions (some of which may not be allowed)
my @visible = Jifty->api->visible_actions;
# Check to see if an action is allowed
if (Jifty->api->is_allowed('TrueFooBar')) {
# do something...
}
# Check to see if an action is visible
if (Jifty->api->is_visible('SpamOurUsers')) {
SpamBot->be_annoying;
}
# Undo all allow/deny/restrict/hide calls
Jifty->api->reset;
=head1 DESCRIPTION
You can fetch an instance of this class by calling L<Jifty/api> in
your application. This object can be used to examine the actions
available within your application and manage access to those actions.
=cut
use base qw/Class::Accessor::Fast Jifty::Object/;
__PACKAGE__->mk_accessors(qw(action_limits));
=head1 METHODS
=head2 new
Creates a new C<Jifty::API> object.
Don't use this, see L<Jifty/api> to access a reference to
C<Jifty::API> in your application.
=cut
sub new {
my $class = shift;
my $self = bless {}, $class;
# Setup the basic allow/deny rules
$self->reset;
# Find all the actions for the API reference (available at __actions)
Jifty::Module::Pluggable->import(
search_path => [
Jifty->app_class("Action"),
"Jifty::Action",
map {ref($_)."::Action"} Jifty->plugins,
],
except => qr/\.#/,
sub_name => "__actions"
);
return ($self);
}
=head2 qualify ACTIONNAME
Returns the fully qualified package name for the given provided
action. If the C<ACTIONNAME> starts with C<Jifty::> or
C<ApplicationClass::Action>, simply returns the given name; otherwise,
it prefixes it with the C<ApplicationClass::Action>.
=cut
sub qualify {
my $self = shift;
my $action = shift;
# Get the application class name
my $base_path = Jifty->config->framework('ApplicationClass');
# Return the class now if it's already fully qualified
return $action
if ($action =~ /^Jifty::/
or $action =~ /^\Q$base_path\E::/);
# Otherwise qualify it
return $base_path . "::Action::" . $action;
}
=head2 reset
Resets which actions are allowed to the defaults; that is, all of the
application's actions, L<Jifty::Action::AboutMe>,
L<Jifty::Action::Autocomplete>, and L<Jifty::Action::Redirect> are allowed and
visible; everything else is denied and hidden. See L</restrict> for the details
of how limits are processed.
=cut
sub reset {
my $self = shift;
# Set up defaults
my $app_actions = Jifty->app_class("Action");
# These are the default action limits
$self->action_limits(
[
{ deny => 1, hide => 1, restriction => qr/.*/ },
{ allow => 1, show => 1, restriction => qr/^\Q$app_actions\E/ },
{ deny => 1, hide => 1, restriction => qr/^\Q$app_actions\E::Record::(Create|Delete|Execute|Search|Update)$/ },
{ allow => 1, show => 1, restriction => 'Jifty::Action::AboutMe' },
{ allow => 1, show => 1, restriction => 'Jifty::Action::Autocomplete' },
{ allow => 1, show => 1, restriction => 'Jifty::Action::Redirect' },
]
);
}
=head2 deny_for_get
Denies all actions except L<Jifty::Action::AboutMe>,
L<Jifty::Action::Autocomplete> and L<Jifty::Action::Redirect>. This is to
protect against a common cross-site scripting hole. In your C<before>
dispatcher rules, you can whitelist actions that are known to be read-only.
This is called automatically during any C<GET> request.
=cut
sub deny_for_get {
my $self = shift;
$self->deny(qr/.*/);
$self->allow("Jifty::Action::AboutMe");
$self->allow("Jifty::Action::Autocomplete");
$self->allow("Jifty::Action::Redirect");
}
=head2 allow RESTRICTIONS
Takes a list of strings or regular expressions, and adds them in order
to the list of limits for the purposes of L</is_allowed>. See
L</restrict> for the details of how limits are processed.
Allowing actions also L</show> them.
=cut
sub allow {
my $self = shift;
$self->restrict( allow => @_ );
}
=head2 deny RESTRICTIONS
Takes a list of strings or regular expressions, and adds them in order
to the list of limits for the purposes of L</is_allowed>. See
L</restrict> for the details of how limits are processed.
=cut
sub deny {
my $self = shift;
$self->restrict( deny => @_ );
}
=head2 hide RESTRICTIONS
Takes a list of strings or regular expressions, and adds them in order
to the list of limits for the purposes of L</is_visible>. See
L</restrict> for the details of how limits are processed.
Hiding actions also L</deny> them.
=cut
sub hide {
my $self = shift;
$self->restrict( hide => @_ );
}
=head2 show RESTRICTIONS
Takes a list of strings or regular expressions, and adds them in order
to the list of limits for the purposes of L</is_visible>. See
L</restrict> for the details of how limits are processed.
=cut
sub show {
my $self = shift;
$self->restrict( show => @_ );
}
=head2 restrict POLARITY RESTRICTIONS
Method that L</allow>, L</deny>, L</hide>, and L</show> call internally;
I<POLARITY> is one of C<allow>, C<deny>, C<hide>, or C<show>. Limits are
evaluated in the order they're called. The last limit that applies will be the
one which takes effect. Regexes are matched against the class; strings are
fully L<qualified|/qualify> and used as an exact match against the class name.
The base set of restrictions (which is reset every request) is set in
L</reset>, and usually modified by the application's L<Jifty::Dispatcher> if
need be.
If you call:
Jifty->api->deny ( qr'Foo' );
Jifty->api->allow ( qr'FooBar' );
Jifty->api->deny ( qr'FooBarDeleteTheWorld' );
..then:
calls to MyApp::Action::Baz will succeed.
calls to MyApp::Action::Foo will fail.
calls to MyApp::Action::FooBar will pass.
calls to MyApp::Action::TrueFoo will fail.
calls to MyApp::Action::TrueFooBar will pass.
calls to MyApp::Action::TrueFooBarDeleteTheWorld will fail.
calls to MyApp::Action::FooBarDeleteTheWorld will fail.
=cut
my %valid_polarity = map { $_ => 1 } qw/allow deny hide show/;
sub restrict {
my $self = shift;
my $polarity = shift;
my @restrictions = @_;
my(undef, $file, $line) = (caller(1));
# Check the sanity of the polarity
die "Polarity must be one of: " . join(', ', sort keys %valid_polarity)
unless $valid_polarity{$polarity};
for my $restriction (@restrictions) {
# Don't let the user "allow .*"
die "For security reasons, Jifty won't let you allow all actions"
if $polarity eq "allow"
and ref $restriction
and $restriction =~ /^\(\?\^?[-xismadlu]*:\^?\.\*\$?\)$/;
# Fully qualify it if it's a string
$restriction = $self->qualify($restriction)
unless ref $restriction;
if ($polarity eq 'hide') {
# Hiding an action also denies it
push @{ $self->action_limits },
{ deny => 1, hide => 1, restriction => $restriction, from => "$file:$line" };
} elsif ($polarity eq 'allow') {
# Allowing an action also shows it
push @{ $self->action_limits },
{ allow => 1, show => 1, restriction => $restriction, from => "$file:$line" };
} else {
# Otherwise, add to list of restrictions unmodified
push @{ $self->action_limits },
{ $polarity => 1, restriction => $restriction, from => "$file:$line" };
}
}
}
=head2 is_allowed CLASS
Returns true if the I<CLASS> name (which is fully qualified if it is
not already) is allowed to be executed. See L</restrict> above for
the rules that the class name must pass.
=cut
sub is_allowed {
my $self = shift;
my $action = shift;
$self->decide_action_polarity($action, 'allow', 'deny');
}
=head2 is_visible CLASS
Returns true if the I<CLASS> name (which is fully qualified if it is
not already) is allowed to be seen. See L</restrict> above for
the rules that the class name must pass.
=cut
sub is_visible {
my $self = shift;
my $action = shift;
$self->decide_action_polarity($action, 'show', 'hide');
}
=head2 decide_action_polarity CLASS, ALLOW, DENY
Returns true if the I<CLASS> name it has the ALLOW restriction, false if it has
the DENY restriction. This is a helper method used by L</is_allowed> and
L</is_visible>.
If no restrictions apply to this action, then false will be returned.
=cut
sub decide_action_polarity {
my $self = shift;
my $class = shift;
my $allow = shift;
my $deny = shift;
# Qualify the action
$class = $self->qualify($class);
# Assume that it doesn't pass; however, the real fallbacks are
# controlled by L</reset>, above.
my $valid = 0;
# Walk all of the limits
for my $limit ( @{ $self->action_limits } ) {
# Regexes are =~ matches, strigns are eq matches
if ( ( ref $limit->{restriction} and $class =~ $limit->{restriction} )
or ( $class eq $limit->{restriction} ) )
{
# If the restriction passes, set the current $allow/$deny
# bit according to if this was a positive or negative
# limit
if ($limit->{$allow}) {
$valid = 1;
}
if ($limit->{$deny}) {
$valid = 0;
}
}
}
return $valid;
}
=head2 explain CLASS
Returns a string describing what allow, deny, show, and hide rules
apply to the class name.
=cut
sub explain {
my $self = shift;
my $class = shift;
$class = $self->qualify($class);
my $str = "";
for my $limit ( @{$self->action_limits} ) {
next unless $limit->{from};
if ( ( ref $limit->{restriction} and $class =~ $limit->{restriction} )
or ( $class eq $limit->{restriction} ) )
{
for my $type (qw/allow deny show hide/) {
$str .= ucfirst($type)." at ".$limit->{from}.", matches ".$limit->{restriction}."\n"
if $limit->{$type};
}
}
}
return $str;
}
=head2 all_actions
Lists the class names of all actions for this Jifty application,
regardless of which are allowed or hidden. See also L</actions> and
L</visible_actions>.
=cut
# Plugin actions under Jifty::Plugin::*::Action are mirrored under
# AppName::Action by Jifty::ClassLoader; this code makes all_actions
# reflect this mirroring.
sub all_actions {
my $self = shift;
unless ( $self->{all_actions} ) {
my @actions = $self->__actions;
my %seen;
$seen{$_}++ for @actions;
for (@actions) {
if (/^Jifty::Plugin::(.*)::Action::(.*)$/) {
my $classname = Jifty->app_class( Action => $2 );
push @actions, $classname unless $seen{$classname};
}
}
$self->{all_actions} = \@actions;
}
return @{ $self->{all_actions} };
}
=head2 actions
Lists the class names of all of the B<allowed> actions for this Jifty
application; this may include actions under the C<Jifty::Action::>
namespace, in addition to your application's actions. See also
L</all_actions> and L</visible_actions>.
=cut
sub actions {
my $self = shift;
return sort grep { $self->is_allowed($_) } $self->all_actions;
}
=head2 visible_actions
Lists the class names of all of the B<visible> actions for this Jifty
application; this may include actions under the C<Jifty::Action::>
namespace, in addition to your application's actions. See also
L</all_actions> and L</actions>.
=cut
sub visible_actions {
my $self = shift;
return sort grep { $self->is_visible($_) } $self->all_actions;
}
=head1 SEE ALSO
L<Jifty>, L<Jifty::Web>, L<Jifty::Action>
=head1 LICENSE
Jifty is Copyright 2005-2010 Best Practical Solutions, LLC.
Jifty is distributed under the same terms as Perl itself.
=cut
1;