1use warnings;
2use strict;
3
4package Jifty::API;
5
6=head1 NAME
7
8Jifty::API - Manages and allow reflection on the Jifty::Actions that
9make up a Jifty application's API
10
11=head1 SYNOPSIS
12
13 # Find the full name of an action
14 my $class = Jifty->api->qualify('SomeAction');
15
16 # New users cannot run some actions
17 if (Jifty->web->current_user->age < 18) {
18     Jifty->api->deny(qr/Vote|PurchaseTobacco/);
19 }
20
21 # Some users cannot even see some actions
22 if (Jifty->web->current_user->id > 10) {
23     Jifty->api->hide('Foo');
24     Jifty->api->show('FooBar');
25     Jifty->api->hide('FooBarDeleteTheWorld');
26 }
27
28 # Fetch the class names of all actions
29 my @actions = Jifty->api->all_actions;
30
31 # Fetch the class names of all the allowed actions
32 my @allowed = Jifty->api->actions;
33
34 # Fetch all of the visible actions (some of which may not be allowed)
35 my @visible = Jifty->api->visible_actions;
36
37 # Check to see if an action is allowed
38 if (Jifty->api->is_allowed('TrueFooBar')) {
39     # do something...
40 }
41
42 # Check to see if an action is visible
43 if (Jifty->api->is_visible('SpamOurUsers')) {
44     SpamBot->be_annoying;
45 }
46
47 # Undo all allow/deny/restrict/hide calls
48 Jifty->api->reset;
49
50=head1 DESCRIPTION
51
52You can fetch an instance of this class by calling L<Jifty/api> in
53your application. This object can be used to examine the actions
54available within your application and manage access to those actions.
55
56=cut
57
58
59use base qw/Class::Accessor::Fast Jifty::Object/;
60
61
62__PACKAGE__->mk_accessors(qw(action_limits));
63
64=head1 METHODS
65
66=head2 new
67
68Creates a new C<Jifty::API> object.
69
70Don't use this, see L<Jifty/api> to access a reference to
71C<Jifty::API> in your application.
72
73=cut
74
75sub new {
76    my $class = shift;
77    my $self  = bless {}, $class;
78
79    # Setup the basic allow/deny rules
80    $self->reset;
81
82    # Find all the actions for the API reference (available at __actions)
83    Jifty::Module::Pluggable->import(
84        search_path => [
85            Jifty->app_class("Action"),
86            "Jifty::Action",
87            map {ref($_)."::Action"} Jifty->plugins,
88        ],
89        except   => qr/\.#/,
90        sub_name => "__actions"
91    );
92
93    return ($self);
94}
95
96=head2 qualify ACTIONNAME
97
98Returns the fully qualified package name for the given provided
99action.  If the C<ACTIONNAME> starts with C<Jifty::> or
100C<ApplicationClass::Action>, simply returns the given name; otherwise,
101it prefixes it with the C<ApplicationClass::Action>.
102
103=cut
104
105sub qualify {
106    my $self   = shift;
107    my $action = shift;
108
109    # Get the application class name
110    my $base_path = Jifty->config->framework('ApplicationClass');
111
112    # Return the class now if it's already fully qualified
113    return $action
114        if ($action =~ /^Jifty::/
115        or $action =~ /^\Q$base_path\E::/);
116
117    # Otherwise qualify it
118    return $base_path . "::Action::" . $action;
119}
120
121=head2 reset
122
123Resets which actions are allowed to the defaults; that is, all of the
124application's actions, L<Jifty::Action::AboutMe>,
125L<Jifty::Action::Autocomplete>, and L<Jifty::Action::Redirect> are allowed and
126visible; everything else is denied and hidden. See L</restrict> for the details
127of how limits are processed.
128
129=cut
130
131sub reset {
132    my $self = shift;
133
134    # Set up defaults
135    my $app_actions = Jifty->app_class("Action");
136
137    # These are the default action limits
138    $self->action_limits(
139        [
140            { deny => 1,  hide => 1, restriction => qr/.*/ },
141            { allow => 1, show => 1, restriction => qr/^\Q$app_actions\E/ },
142            { deny => 1,  hide => 1, restriction => qr/^\Q$app_actions\E::Record::(Create|Delete|Execute|Search|Update)$/ },
143            { allow => 1, show => 1, restriction => 'Jifty::Action::AboutMe' },
144            { allow => 1, show => 1, restriction => 'Jifty::Action::Autocomplete' },
145            { allow => 1, show => 1, restriction => 'Jifty::Action::Redirect' },
146        ]
147    );
148}
149
150=head2 deny_for_get
151
152Denies all actions except L<Jifty::Action::AboutMe>,
153L<Jifty::Action::Autocomplete> and L<Jifty::Action::Redirect>. This is to
154protect against a common cross-site scripting hole. In your C<before>
155dispatcher rules, you can whitelist actions that are known to be read-only.
156
157This is called automatically during any C<GET> request.
158
159=cut
160
161sub deny_for_get {
162    my $self = shift;
163    $self->deny(qr/.*/);
164    $self->allow("Jifty::Action::AboutMe");
165    $self->allow("Jifty::Action::Autocomplete");
166    $self->allow("Jifty::Action::Redirect");
167}
168
169=head2 allow RESTRICTIONS
170
171Takes a list of strings or regular expressions, and adds them in order
172to the list of limits for the purposes of L</is_allowed>.  See
173L</restrict> for the details of how limits are processed.
174
175Allowing actions also L</show> them.
176
177=cut
178
179sub allow {
180    my $self = shift;
181    $self->restrict( allow => @_ );
182}
183
184=head2 deny RESTRICTIONS
185
186Takes a list of strings or regular expressions, and adds them in order
187to the list of limits for the purposes of L</is_allowed>.  See
188L</restrict> for the details of how limits are processed.
189
190=cut
191
192sub deny {
193    my $self = shift;
194    $self->restrict( deny => @_ );
195}
196
197=head2 hide RESTRICTIONS
198
199Takes a list of strings or regular expressions, and adds them in order
200to the list of limits for the purposes of L</is_visible>.  See
201L</restrict> for the details of how limits are processed.
202
203Hiding actions also L</deny> them.
204
205=cut
206
207sub hide {
208    my $self = shift;
209    $self->restrict( hide => @_ );
210}
211
212=head2 show RESTRICTIONS
213
214Takes a list of strings or regular expressions, and adds them in order
215to the list of limits for the purposes of L</is_visible>.  See
216L</restrict> for the details of how limits are processed.
217
218=cut
219
220sub show {
221    my $self = shift;
222    $self->restrict( show => @_ );
223}
224
225=head2 restrict POLARITY RESTRICTIONS
226
227Method that L</allow>, L</deny>, L</hide>, and L</show> call internally;
228I<POLARITY> is one of C<allow>, C<deny>, C<hide>, or C<show>. Limits are
229evaluated in the order they're called. The last limit that applies will be the
230one which takes effect. Regexes are matched against the class; strings are
231fully L<qualified|/qualify> and used as an exact match against the class name.
232The base set of restrictions (which is reset every request) is set in
233L</reset>, and usually modified by the application's L<Jifty::Dispatcher> if
234need be.
235
236If you call:
237
238    Jifty->api->deny  ( qr'Foo' );
239    Jifty->api->allow ( qr'FooBar' );
240    Jifty->api->deny  ( qr'FooBarDeleteTheWorld' );
241
242..then:
243
244    calls to MyApp::Action::Baz will succeed.
245    calls to MyApp::Action::Foo will fail.
246    calls to MyApp::Action::FooBar will pass.
247    calls to MyApp::Action::TrueFoo will fail.
248    calls to MyApp::Action::TrueFooBar will pass.
249    calls to MyApp::Action::TrueFooBarDeleteTheWorld will fail.
250    calls to MyApp::Action::FooBarDeleteTheWorld will fail.
251
252=cut
253
254my %valid_polarity = map { $_ => 1 } qw/allow deny hide show/;
255
256sub restrict {
257    my $self         = shift;
258    my $polarity     = shift;
259    my @restrictions = @_;
260
261    my(undef, $file, $line) = (caller(1));
262
263    # Check the sanity of the polarity
264    die "Polarity must be one of: " . join(', ', sort keys %valid_polarity)
265        unless $valid_polarity{$polarity};
266
267    for my $restriction (@restrictions) {
268
269        # Don't let the user "allow .*"
270        die "For security reasons, Jifty won't let you allow all actions"
271            if $polarity eq "allow"
272            and ref $restriction
273            and $restriction =~ /^\(\?\^?[-xismadlu]*:\^?\.\*\$?\)$/;
274
275        # Fully qualify it if it's a string
276        $restriction = $self->qualify($restriction)
277            unless ref $restriction;
278
279
280        if ($polarity eq 'hide') {
281            # Hiding an action also denies it
282            push @{ $self->action_limits },
283                { deny => 1, hide => 1, restriction => $restriction, from => "$file:$line" };
284        } elsif ($polarity eq 'allow') {
285            # Allowing an action also shows it
286            push @{ $self->action_limits },
287                { allow => 1, show => 1, restriction => $restriction, from => "$file:$line" };
288        } else {
289            # Otherwise, add to list of restrictions unmodified
290            push @{ $self->action_limits },
291                { $polarity => 1, restriction => $restriction, from => "$file:$line" };
292        }
293    }
294}
295
296=head2 is_allowed CLASS
297
298Returns true if the I<CLASS> name (which is fully qualified if it is
299not already) is allowed to be executed.  See L</restrict> above for
300the rules that the class name must pass.
301
302=cut
303
304sub is_allowed {
305    my $self   = shift;
306    my $action = shift;
307
308    $self->decide_action_polarity($action, 'allow', 'deny');
309}
310
311=head2 is_visible CLASS
312
313Returns true if the I<CLASS> name (which is fully qualified if it is
314not already) is allowed to be seen.  See L</restrict> above for
315the rules that the class name must pass.
316
317=cut
318
319sub is_visible {
320    my $self   = shift;
321    my $action = shift;
322
323    $self->decide_action_polarity($action, 'show', 'hide');
324}
325
326=head2 decide_action_polarity CLASS, ALLOW, DENY
327
328Returns true if the I<CLASS> name it has the ALLOW restriction, false if it has
329the DENY restriction. This is a helper method used by L</is_allowed> and
330L</is_visible>.
331
332If no restrictions apply to this action, then false will be returned.
333
334=cut
335
336sub decide_action_polarity {
337    my $self  = shift;
338    my $class = shift;
339    my $allow = shift;
340    my $deny  = shift;
341
342    # Qualify the action
343    $class = $self->qualify($class);
344
345    # Assume that it doesn't pass; however, the real fallbacks are
346    # controlled by L</reset>, above.
347    my $valid = 0;
348
349    # Walk all of the limits
350    for my $limit ( @{ $self->action_limits } ) {
351
352        # Regexes are =~ matches, strigns are eq matches
353        if ( ( ref $limit->{restriction} and $class =~ $limit->{restriction} )
354            or ( $class eq $limit->{restriction} ) )
355        {
356
357            # If the restriction passes, set the current $allow/$deny
358            # bit according to if this was a positive or negative
359            # limit
360            if ($limit->{$allow}) {
361                $valid = 1;
362            }
363            if ($limit->{$deny}) {
364                $valid = 0;
365            }
366        }
367    }
368
369    return $valid;
370}
371
372=head2 explain CLASS
373
374Returns a string describing what allow, deny, show, and hide rules
375apply to the class name.
376
377=cut
378
379sub explain {
380    my $self = shift;
381    my $class = shift;
382
383    $class = $self->qualify($class);
384
385    my $str = "";
386    for my $limit ( @{$self->action_limits} ) {
387        next unless $limit->{from};
388        if ( ( ref $limit->{restriction} and $class =~ $limit->{restriction} )
389            or ( $class eq $limit->{restriction} ) )
390        {
391            for my $type (qw/allow deny show hide/) {
392                $str .= ucfirst($type)." at ".$limit->{from}.", matches ".$limit->{restriction}."\n"
393                    if $limit->{$type};
394            }
395        }
396    }
397    return $str;
398}
399
400=head2 all_actions
401
402Lists the class names of all actions for this Jifty application,
403regardless of which are allowed or hidden.  See also L</actions> and
404L</visible_actions>.
405
406=cut
407
408# Plugin actions under Jifty::Plugin::*::Action are mirrored under
409# AppName::Action by Jifty::ClassLoader; this code makes all_actions
410# reflect this mirroring.
411sub all_actions {
412    my $self = shift;
413    unless ( $self->{all_actions} ) {
414        my @actions = $self->__actions;
415        my %seen;
416        $seen{$_}++ for @actions;
417        for (@actions) {
418            if (/^Jifty::Plugin::(.*)::Action::(.*)$/) {
419                my $classname = Jifty->app_class( Action => $2 );
420                push @actions, $classname unless $seen{$classname};
421            }
422        }
423        $self->{all_actions} = \@actions;
424    }
425    return @{ $self->{all_actions} };
426}
427
428=head2 actions
429
430Lists the class names of all of the B<allowed> actions for this Jifty
431application; this may include actions under the C<Jifty::Action::>
432namespace, in addition to your application's actions.  See also
433L</all_actions> and L</visible_actions>.
434
435=cut
436
437sub actions {
438    my $self = shift;
439    return sort grep { $self->is_allowed($_) } $self->all_actions;
440}
441
442=head2 visible_actions
443
444Lists the class names of all of the B<visible> actions for this Jifty
445application; this may include actions under the C<Jifty::Action::>
446namespace, in addition to your application's actions.  See also
447L</all_actions> and L</actions>.
448
449=cut
450
451sub visible_actions {
452    my $self = shift;
453    return sort grep { $self->is_visible($_) } $self->all_actions;
454}
455
456=head1 SEE ALSO
457
458L<Jifty>, L<Jifty::Web>, L<Jifty::Action>
459
460=head1 LICENSE
461
462Jifty is Copyright 2005-2010 Best Practical Solutions, LLC.
463Jifty is distributed under the same terms as Perl itself.
464
465=cut
466
4671;
468