Add routes-based access control

This commit is contained in:
Dmitriy Simushev 2014-05-13 12:13:02 +00:00
parent bac1404cf1
commit b10408b631
2 changed files with 188 additions and 5 deletions

View File

@ -0,0 +1,95 @@
<?php
/*
* Copyright 2005-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Mibew\AccessControl\Check;
use Symfony\Component\HttpFoundation\Request;
class CheckResolver
{
/**
* Resolves access check callable by request.
*
* @param Request $request Incoming request.
* @return callable
* @throws \InvalidArgumentException If the access check cannot be resolved.
*/
public function getCheck(Request $request)
{
// Get access check name from the request
$access_check = $request->attributes->get('_access_check');
if (!$access_check) {
// By default we do not need to restrict access
return function () {
return true;
};
}
// Check if specified access check is something that can be called
// directly
if (strpos($access_check, ':') === false) {
if (method_exists($access_check, '__invoke')) {
return new $access_check();
} elseif (function_exists($access_check)) {
return $access_check;
} else {
throw new \InvalidArgumentException(sprintf(
'Unable to find access check "%s".',
$access_check
));
}
}
// Build callable for specified access check
$callable = $this->createCheck($access_check);
if (!is_callable($callable)) {
throw new \InvalidArgumentException(sprintf(
'Access check "%s" for URI "%s" is not callable.',
$access_check,
$request->getPathInfo()
));
}
return $callable;
}
/**
* Builds access check callable by its full name.
*
* @param string $access_check Full access check name in "<Class>::<method>"
* format.
* @return callable Access check callable
* @throws \InvalidArgumentException
*/
protected function createCheck($access_check)
{
if (strpos($access_check, '::') === false) {
throw new \InvalidArgumentException(sprintf(
'Unable to find access check "%s".',
$access_check
));
}
list($class, $method) = explode('::', $access_check, 2);
if (!class_exists($class)) {
throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
}
return array(new $class(), $method);
}
}

View File

@ -17,12 +17,15 @@
namespace Mibew;
use Mibew\EventDispatcher;
use Mibew\Routing\Router;
use Mibew\Routing\RouteCollectionLoader;
use Mibew\Routing\Exception\AccessDeniedException;
use Mibew\Controller\ControllerResolver;
use Mibew\AccessControl\Check\CheckResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
@ -56,6 +59,7 @@ class Application
$this->fileLocator = new FileLocator(array(MIBEW_FS_ROOT));
$this->router = new Router(new RouteCollectionLoader($this->fileLocator));
$this->controllerResolver = new ControllerResolver($this->router);
$this->accessCheckResolver = new CheckResolver();
}
/**
@ -72,17 +76,22 @@ class Application
$this->router->setContext($context);
try {
// Try to match route
// Try to match a route and add extra data to the request.
$parameters = $this->router->matchRequest($request);
$request->attributes->add($parameters);
$request->attributes->set('_operator', $this->extractOperator($request));
// Get controller
// Check if the user can access the page
$access_check = $this->accessCheckResolver->getCheck($request);
if (!call_user_func($access_check, $request)) {
throw new AccessDeniedException();
}
// Get controller and perform its action to get a response.
$controller = $this->controllerResolver->getController($request);
// Execute the controller's action and get response.
$response = call_user_func($controller, $request);
} catch (AccessDeniedException $e) {
return new Response('Forbidden', 403);
return $this->buildAccessDeniedResponse($request);
} catch (ResourceNotFoundException $e) {
return new Response('Not Found', 404);
} catch (MethodNotAllowedException $e) {
@ -99,4 +108,83 @@ class Application
return new Response((string)$response);
}
}
/**
* Extracts operator's data from the passed in request object.
*
* @param Request $request A request to extract operator from.
* @return array|bool Associative array with operator's data or boolean
* false if there is no operator related with the request.
*
* @todo Remove this method when Object Oriented wrapper for an operator
* will be created.
*/
protected function extractOperator(Request $request)
{
// Try to get operator from session.
if (isset($_SESSION[SESSION_PREFIX . "operator"])) {
return $_SESSION[SESSION_PREFIX . "operator"];
}
// Check if operator had used "remember me" feature.
if ($request->cookies->has(REMEMBER_OPERATOR_COOKIE_NAME)) {
$cookie_value = $request->cookies->get(REMEMBER_OPERATOR_COOKIE_NAME);
list($login, $pwd) = preg_split('/\x0/', base64_decode($cookie_value), 2);
$op = operator_by_login($login);
$can_login = $op
&& isset($pwd)
&& isset($op['vcpassword'])
&& calculate_password_hash($op['vclogin'], $op['vcpassword']) == $pwd
&& !operator_is_disabled($op);
if ($can_login) {
$_SESSION[SESSION_PREFIX . "operator"] = $op;
return $op;
}
}
// Operator's data cannot be extracted from the request.
return false;
}
/**
* Builds response for pages with denied access
*
* Triggers "accessDenied' event to provide an ability for plugins to set custom response.
* an associative array with folloing keys is passed to event listeners:
* - 'request': {@link Symfony\Component\HttpFoundation\Request} object.
*
* An event listener can attach custom response to the arguments array
* (using "response" key) to send it to the client.
*
* @param Request $request Incoming request
* @return Response
*/
protected function buildAccessDeniedResponse(Request $request)
{
// Trigger fail
$args = array(
'request' => $request,
'response' => false,
);
$dispatcher = EventDispatcher::getInstance();
$dispatcher->triggerEvent('accessDenied', $args);
if ($args['response'] && ($args['response'] instanceof Response)) {
// If one of event listeners returned the response object send it
// to the client.
return $args['response'];
}
if ($request->attributes->get('_operator')) {
// If the operator already logged in, display 403 page.
return new Response('Forbidden', 403);
}
// Operator is not logged in. Redirect him to the login page.
$_SESSION['backpath'] = $request->getUri();
$response = new RedirectResponse($request->getUriForPath('/operator/login.php'));
return $response;
}
}