From: Vincent Zanotti Date: Mon, 3 Jan 2011 18:57:53 +0000 (+0100) Subject: Adds a new PlApiHook for API handlers. The main two differences with X-Git-Tag: core/1.1.2~14 X-Git-Url: http://git.polytechnique.org/?a=commitdiff_plain;h=504647c51aac2a34a4c3f11f33cc4fa6eecdb64f;p=platal.git Adds a new PlApiHook for API handlers. The main two differences with PlTokenHook is that (a) it provides stronger authentication, and (b) it parses the JSON request payload before passing it to the handler. Signed-off-by: Vincent Zanotti --- diff --git a/classes/platal.php b/classes/platal.php index cbd9870..be22b6f 100644 --- a/classes/platal.php +++ b/classes/platal.php @@ -108,6 +108,86 @@ class PlStdHook extends PlHook } } +/** A specialized hook for API requests. + * It is intended to be used for passive API requests, authenticated either by + * an existing session (with a valid XSRF token), or by an alternative single + * request auth mechanism implemented by PlSession::apiAuth. + * + * This hook is suitable for read-write requests against the website, provided + * $auth is set appropriately. Note that the auth level is only checked for + * session-authenticated users, as "apiAuth" users are assumed to always have + * the requested level (use another hook otherwise). + * + * The callback will be passed as arguments the PlPage, the authenticated + * PlUser, the JSON decoded payload, and the remaining path components, as with + * any other hook. + * + * If the callback intends to JSON-encode its returned value, it is advised to + * use PlPage::jsonAssign, and return PL_JSON to enable automatic encoding. + */ +class PlApiHook extends PlHook +{ + private $actualAuth; + private $callback; + + public function __construct($callback, $auth = AUTH_PUBLIC, $perms = 'user', $type = NO_AUTH) + { + // As mentioned above, $auth is only applied for session-based auth + // (as opposed to token-based). PlHook is initialized to AUTH_PUBLIC to + // avoid it refusing to approve requests; this is important as the user + // is not yet authenticated at that point (see below for the actual + // permissions check). + parent::__construct(AUTH_PUBLIC, $perms, $type); + $this->actualAuth = $auth; + $this->callback = $callback; + } + + private function getEncodedPayload($method) + { + return $method == "GET" ? "" : file_get_contents("php://input"); + } + + private function decodePayload($encodedPayload) + { + return empty($encodedPayload) ? array() : json_decode($encodedPayload, true); + } + + protected function run(PlPage &$page, array $args) + { + $method = $_SERVER['REQUEST_METHOD']; + $encodedPayload = $this->getEncodedPayload($method); + $jsonPayload = $this->decodePayload($encodedPayload); + $resource = '/' . implode('/', $args); + + // If the payload wasn't a valid JSON encoded object, bail out early. + if (is_null($jsonPayload)) { + $page->trigError("Could not decode the JSON-encoded payload sent with the request."); + return PL_BAD_REQUEST; + } + + // Authenticate the request. Try first with the existing session (which + // is less expensive to check), by veryfing that the XSRF token is + // valid; otherwise fallbacks to API-type authentication from PlSession. + if (S::logged() && S::has_xsrf_token() && Platal::session()->checkAuth($this->actualAuth)) { + $user = S::user(); + } else { + $user = Platal::session()->apiAuth($method, $resource, $encodedPayload); + } + + // Check the permissions, unless the handler is fully public. + if ($this->actualAuth > AUTH_PUBLIC) { + if (is_null($user) || !$user->checkPerms($this->perms)) { + return PL_FORBIDDEN; + } + } + + // Invoke the callback, whose signature is (PlPage, PlUser, jsonPayload). + array_shift($args); + array_unshift($args, $page, $user, $jsonPayload); + return call_user_func_array($this->callback, $args); + } +} + /** A specialized hook for token-based requests. * It is intended for purely passive requests (typically for serving CSV or RSS * content outside the browser), and can fallback to regular session-based @@ -129,11 +209,7 @@ class PlTokenHook extends PlHook public function __construct($callback, $auth = AUTH_PUBLIC, $perms = 'user', $type = NO_AUTH) { - // As mentioned above, $auth is only applied for session-based auth - // (as opposed to token-based). PlHook is initialized to AUTH_PUBLIC to - // avoid it refusing to approve requests; this is important as the user - // is not yet authenticated at that point (see below for the actual - // permissions check). + // See PlApiHook::__construct. parent::__construct(AUTH_PUBLIC, $perms, $type); $this->actualAuth = $auth; $this->callback = $callback; diff --git a/classes/plmodule.php b/classes/plmodule.php index 92071b8..e60b9f0 100644 --- a/classes/plmodule.php +++ b/classes/plmodule.php @@ -55,6 +55,29 @@ abstract class PLModule return new PlStdHook(array($this, 'handler_' . $fun), $auth, $perms, $type); } + /** Register an API hook. + * @param fun name of the handler (the exact name will be handler_$fun); the + * handler will be invoked with a PlPage, the authenticated PlUser, the + * JSON-decoded payload (if any), and the unmatched path components + * @param auth authentification level required, when not API-authenticated + * @param perms permission required to run this handler + * @param type additionnal flags (only NO_HTTPS is supported at the moment) + * + * See {@link make_hook} above for details on permissions. + * + * WARNING: It is expected that the API authentication mechanism will not be + * protected against short-term replay of requests (for instance replay of a + * given request within 5-10 seconds). + * + * You are explicitly advised to make any API request idempotent (eg. use + * "DELETE /api/email/foo@example.com" instead of "DELETE /api/email/0" to + * delete the first email in a list). + */ + public function make_api_hook($fun, $auth, $perms = 'user', $type = NO_AUTH) + { + return new PlApiHook(array($this, 'handler_' . $fun), $auth, $perms, $type); + } + /** Register a token-authentified hook (rss, csv, ical, ...) * @param fun name of the handler (the exact name will be handler_$fun); the * handler will be invoked with the PlPage object, the PlUser of the diff --git a/classes/plsession.php b/classes/plsession.php index 7a243be..ef79945 100644 --- a/classes/plsession.php +++ b/classes/plsession.php @@ -159,6 +159,23 @@ abstract class PlSession */ abstract protected function startSessionAs($user, $level); + /** Authenticate the request for the given (method, payload) pair. + * + * Implementations are expected to provide strong authentication. It is + * suggested to use an HMAC-based scheme, where the signature validates the + * method, url, and payload (to avoid replay of the signature against other + * methods), and the timestamp (to avoid replay in time). + * + * @param method method of the request (GET, POST, PUT, DELETE) + * @param resource URL path of the resource (eg. "/api/user") + * @param payload binary payload sent with the request (before decoding) + * @return a valid PlUser object if authentication is successfull, or null. + */ + public function apiAuth($method, $resource, $payload) + { + return null; // Default implementation does nothing + } + /** Check authentication with the given token. * * Token authentication is a light-weight authentication based on a user-specific token.