diff --git a/docs/en/howto/csv-import.md b/docs/en/howto/csv-import.md
index 5c8253397d3..b53ce3c0264 100644
--- a/docs/en/howto/csv-import.md
+++ b/docs/en/howto/csv-import.md
@@ -68,6 +68,9 @@ You can have more customized logic and interface feedback through a custom contr
:::php
renderWith('MyTemplate');
}
diff --git a/docs/en/reference/grid-field.md b/docs/en/reference/grid-field.md
index 465786a1efe..6d4aeb2ffd8 100644
--- a/docs/en/reference/grid-field.md
+++ b/docs/en/reference/grid-field.md
@@ -41,6 +41,8 @@ Here is an example where we display a basic gridfield with the default settings:
:::php
class GridController extends Page_Controller {
+
+ static $allowed_actions = array('index');
public function index(SS_HTTPRequest $request) {
$this->Content = $this->AllPages();
diff --git a/docs/en/reference/templates.md b/docs/en/reference/templates.md
index 1fe556ddeba..81b3ca0647a 100644
--- a/docs/en/reference/templates.md
+++ b/docs/en/reference/templates.md
@@ -568,6 +568,8 @@ default if it exists and there is no action in the url parameters.
:::php
class MyPage_Controller extends Page_Controller {
+
+ static $allowed_actions = array('index');
public function init(){
parent::init();
diff --git a/docs/en/topics/controller.md b/docs/en/topics/controller.md
index 268e4aaf6df..cfd80041069 100644
--- a/docs/en/topics/controller.md
+++ b/docs/en/topics/controller.md
@@ -3,7 +3,7 @@
Base controller class. You will extend this to take granular control over the
actions and url handling of aspects of your SilverStripe site.
-## Example
+## Usage
The following example is for a simple `[api:Controller]` class. If you're using
the cms module and looking at Page_Controller instances you won't need to setup
@@ -15,11 +15,14 @@ your own routes since the cms module handles these routes.
cheesefries
)
+
+ SilverStripe automatically adds a URL routing entry based on the controller's class name,
+ so a `MyController` class is accessible through `http://yourdomain.com/MyController`.
+
+
+## Access Control
+
+### Through $allowed_actions
+
+All public methods on a controller are accessible by their name through the `$Action`
+part of the URL routing, so a `MyController->mymethod()` is accessible at
+`http://yourdomain.com/MyController/mymethod`. This is not always desireable,
+since methods can return internal information, or change state in a way
+that's not intended to be used through a URL endpoint.
+
+SilverStripe strongly recommends securing your controllers
+through defining a `$allowed_actions` array on the class,
+which allows whitelisting of methods, as well as a concise
+way to perform checks against permission codes or custom logic.
+
+ :::php
+ class MyController extends Controller {
+ public static $allowed_actions = array(
+ // someaction can be accessed by anyone, any time
+ 'someaction',
+ // So can otheraction
+ 'otheraction' => true,
+ // restrictedaction can only be people with ADMIN privilege
+ 'restrictedaction' => 'ADMIN',
+ // complexaction can only be accessed if $this->canComplexAction() returns true
+ 'complexaction' '->canComplexAction'
+ );
+ }
+
+There's a couple of rules guiding these checks:
+
+ * Each controller is only responsible for access control on the methods it defines
+ * If a method on a parent class is overwritten, access control for it has to be redefined as well
+ * An action named "index" is whitelisted by default
+ * A wildcard (`*`) can be used to define access control for all methods (incl. methods on parent classes)
+ * Specific method entries in `$allowed_actions` overrule any `*` settings
+ * Methods returning forms also count as actions which need to be defined
+ * Form action methods (targets of `FormAction`) should NOT be included in `$allowed_actions`,
+ they're handled separately through the form routing (see the ["forms" topic](/topics/forms))
+ * `$allowed_actions` can be defined on `Extension` classes applying to the controller.
+
+
+If the permission check fails, SilverStripe will return a "403 Forbidden" HTTP status.
+
+### Through the action
+
+Each method responding to a URL can also implement custom permission checks,
+e.g. to handle responses conditionally on the passed request data.
+
+ :::php
+ class MyController extends Controller {
+ public static $allowed_actions = array('myaction');
+ public function myaction($request) {
+ if(!$request->getVar('apikey')) {
+ return $this->httpError(403, 'No API key provided');
+ }
+
+ return 'valid';
+ }
+ }
+
+Unless you transform the response later in the request processing,
+it'll look pretty ugly to the user. Alternatively, you can use
+`ErrorPage::response_for()` to return a more specialized layout.
+
+Note: This is recommended as an addition for `$allowed_actions`, in order to handle
+more complex checks, rather than a replacement.
+
+### Through the init() method
+
+After checking for allowed_actions, each controller invokes its `init()` method,
+which is typically used to set up common state in the controller, and
+include JavaScript and CSS files in the output which are used for any action.
+If an `init()` method returns a `SS_HTTPResponse` with either a 3xx or 4xx HTTP
+status code, it'll abort execution. This behaviour can be used to implement
+permission checks.
+
+ :::php
+ class MyController extends Controller {
+ public static $allowed_actions = array();
+ public function init() {
+ parent::init();
+ if(!Permission::check('ADMIN')) return $this->httpError(403);
+ }
+ }
## URL Handling
@@ -60,7 +153,7 @@ through `/fastfood/drivethrough/` to use the same order function.
:::php
class FastFood_Controller extends Controller {
-
+ static $allowed_actions = array('drivethrough');
public static $url_handlers = array(
'drivethrough/$Action/$ID/$Name' => 'order'
);
diff --git a/docs/en/topics/form-validation.md b/docs/en/topics/form-validation.md
index ccd7906f9e3..a99a3b4219f 100644
--- a/docs/en/topics/form-validation.md
+++ b/docs/en/topics/form-validation.md
@@ -49,6 +49,7 @@ Example: Validate postcodes based on the selected country (on the controller).
:::php
class MyController extends Controller {
+ static $allowed_actions = array('Form');
public function Form() {
return Form::create($this, 'Form',
new FieldList(
diff --git a/docs/en/topics/forms.md b/docs/en/topics/forms.md
index 5844912e807..5ac3189f3bf 100644
--- a/docs/en/topics/forms.md
+++ b/docs/en/topics/forms.md
@@ -25,9 +25,7 @@ Forms start at the controller. Here is an simple example on how to set up a form
:::php
class Page_Controller extends ContentController {
- public static $allowed_actions = array(
- 'HelloForm',
- );
+ public static $allowed_actions = array('HelloForm');
// Template method
public function HelloForm() {
@@ -41,11 +39,24 @@ Forms start at the controller. Here is an simple example on how to set up a form
return $form;
}
- public function doSayHello(array $data, Form $form) {
+ public function doSayHello($data, Form $form) {
// Do something with $data
return $this->render();
}
}
+
+The name of the form ("HelloForm") is passed into the `Form`
+constructor as a second argument. It needs to match the method name.
+
+Since forms need a URL, the `HelloForm()` method needs to be handled
+like any other controller action. In order to whitelist its access through
+URLs, we add it to the `$allowed_actions` array.
+Form actions ("doSayHello") on the other hand should NOT be included here,
+these are handled separately through `Form->httpSubmission()`.
+You can control access on form actions either by conditionally removing
+a `FormAction` from the form construction,
+or by defining `$allowed_actions` in your own `Form` class
+(more information in the ["controllers" topic](/topics/controllers)).
**Page.ss**
diff --git a/docs/en/topics/security.md b/docs/en/topics/security.md
index fff3e792d38..efd48b67ff0 100644
--- a/docs/en/topics/security.md
+++ b/docs/en/topics/security.md
@@ -79,6 +79,7 @@ Example:
:::php
class MyController extends Controller {
+ static $allowed_actions = array('myurlaction');
public function myurlaction($RAW_urlParams) {
$SQL_urlParams = Convert::raw2sql($RAW_urlParams); // works recursively on an array
$objs = Player::get()->where("Name = '{$SQL_data[OtherID]}'");
@@ -93,7 +94,6 @@ This means if you've got a chain of functions passing data through, escaping sho
:::php
class MyController extends Controller {
/**
-
* @param array $RAW_data All names in an indexed array (not SQL-safe)
*/
public function saveAllNames($RAW_data) {
@@ -220,6 +220,7 @@ PHP:
:::php
class MyController extends Controller {
+ static $allowed_actions = array('search');
public function search($request) {
$htmlTitle = 'Your results for:' . Convert::raw2xml($request->getVar('Query')) . '
';
return $this->customise(array(
@@ -249,6 +250,7 @@ PHP:
:::php
class MyController extends Controller {
+ static $allowed_actions = array('search');
public function search($request) {
$rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc";
$rssLink = Controller::join_links($this->Link(), $rssRelativeLink);
diff --git a/docs/en/tutorials/3-forms.md b/docs/en/tutorials/3-forms.md
index 8f8435660de..635014f1cfb 100644
--- a/docs/en/tutorials/3-forms.md
+++ b/docs/en/tutorials/3-forms.md
@@ -21,6 +21,8 @@ The poll we will be creating on our homepage will ask the user for their name an
:::php
class HomePage_Controller extends Page_Controller {
+ static $allowed_actions = array('BrowserPollForm');
+
// ...
public function BrowserPollForm() {