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 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 object. Don't use this, see L to access a reference to C 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 starts with C or C, simply returns the given name; otherwise, it prefixes it with the C. =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, L, and L are allowed and visible; everything else is denied and hidden. See L 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, L and L. This is to protect against a common cross-site scripting hole. In your C dispatcher rules, you can whitelist actions that are known to be read-only. This is called automatically during any C 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. See L for the details of how limits are processed. Allowing actions also L 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. See L 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. See L for the details of how limits are processed. Hiding actions also L 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. See L for the details of how limits are processed. =cut sub show { my $self = shift; $self->restrict( show => @_ ); } =head2 restrict POLARITY RESTRICTIONS Method that L, L, L, and L call internally; I is one of C, C, C, or C. 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 and used as an exact match against the class name. The base set of restrictions (which is reset every request) is set in L, and usually modified by the application's L 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 name (which is fully qualified if it is not already) is allowed to be executed. See L 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 name (which is fully qualified if it is not already) is allowed to be seen. See L 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 name it has the ALLOW restriction, false if it has the DENY restriction. This is a helper method used by L and L. 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, 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 and L. =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 actions for this Jifty application; this may include actions under the C namespace, in addition to your application's actions. See also L and L. =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 actions for this Jifty application; this may include actions under the C namespace, in addition to your application's actions. See also L and L. =cut sub visible_actions { my $self = shift; return sort grep { $self->is_visible($_) } $self->all_actions; } =head1 SEE ALSO L, L, L =head1 LICENSE Jifty is Copyright 2005-2010 Best Practical Solutions, LLC. Jifty is distributed under the same terms as Perl itself. =cut 1;