API: Add authz features for RESTBase
authorBrad Jorsch <bjorsch@wikimedia.org>
Thu, 29 Jan 2015 20:14:40 +0000 (12:14 -0800)
committerBrad Jorsch <bjorsch@wikimedia.org>
Thu, 19 Feb 2015 21:45:03 +0000 (16:45 -0500)
The RESTBase team has requested the ability to check the validity of a
CSRF token and to interface with Title::userCan().

The former is accomplished by the new action=checktoken module. The
latter by a new parameter ('testactions') to the existing prop=info.

Bug: T88010
Change-Id: I2530f1315ec93f5be9fb437137992150fdc305f2

RELEASE-NOTES-1.25
autoload.php
includes/User.php
includes/api/ApiCheckToken.php [new file with mode: 0644]
includes/api/ApiMain.php
includes/api/ApiQueryInfo.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json

index 8992ce0..86395ac 100644 (file)
@@ -213,6 +213,9 @@ production.
 * list=tags has additional properties to indicate 'active' status and tag
   sources.
 * siprop=libraries was added to ApiQuerySiteInfo to list installed external libraries.
+* (T88010) Added action=checktoken, to test a CSRF token's validity.
+* (T88010) Added intestactions to prop=info, to allow querying of
+  Title::userCan() via the API.
 
 === Action API internal changes in 1.25 ===
 * ApiHelp has been rewritten to support i18n and paginated HTML output.
index 01dba44..bf759bc 100644 (file)
@@ -18,6 +18,7 @@ $wgAutoloadLocalClasses = array(
        'AnsiTermColorer' => __DIR__ . '/maintenance/term/MWTerm.php',
        'ApiBase' => __DIR__ . '/includes/api/ApiBase.php',
        'ApiBlock' => __DIR__ . '/includes/api/ApiBlock.php',
+       'ApiCheckToken' => __DIR__ . '/includes/api/ApiCheckToken.php',
        'ApiClearHasMsg' => __DIR__ . '/includes/api/ApiClearHasMsg.php',
        'ApiComparePages' => __DIR__ . '/includes/api/ApiComparePages.php',
        'ApiCreateAccount' => __DIR__ . '/includes/api/ApiCreateAccount.php',
index c2db67a..ae8deb6 100644 (file)
@@ -3937,6 +3937,20 @@ class User implements IDBAccessObject {
                return MWCryptRand::generateHex( 32 );
        }
 
+       /**
+        * Get the embedded timestamp from a token.
+        * @param string $val Input token
+        * @return int|null
+        */
+       public static function getEditTokenTimestamp( $val ) {
+               $suffixLen = strlen( self::EDIT_TOKEN_SUFFIX );
+               if ( strlen( $val ) <= 32 + $suffixLen ) {
+                       return null;
+               }
+
+               return hexdec( substr( $val, 32, -$suffixLen ) );
+       }
+
        /**
         * Check given value against the token value stored in the session.
         * A match should confirm that the form was submitted from the
@@ -3954,12 +3968,10 @@ class User implements IDBAccessObject {
                        return $val === self::EDIT_TOKEN_SUFFIX;
                }
 
-               $suffixLen = strlen( self::EDIT_TOKEN_SUFFIX );
-               if ( strlen( $val ) <= 32 + $suffixLen ) {
+               $timestamp = self::getEditTokenTimestamp( $val );
+               if ( $timestamp === null ) {
                        return false;
                }
-
-               $timestamp = hexdec( substr( $val, 32, -$suffixLen ) );
                if ( $maxage !== null && $timestamp < wfTimestamp() - $maxage ) {
                        // Expired token
                        return false;
diff --git a/includes/api/ApiCheckToken.php b/includes/api/ApiCheckToken.php
new file mode 100644 (file)
index 0000000..28c6ece
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Created on Jan 29, 2015
+ *
+ * Copyright © 2015 Brad Jorsch bjorsch@wikimedia.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @since 1.25
+ * @ingroup API
+ */
+class ApiCheckToken extends ApiBase {
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+               $token = $params['token'];
+               $maxage = $params['maxtokenage'];
+               $request = $this->getRequest();
+               $salts = ApiQueryTokens::getTokenTypeSalts();
+               $salt = $salts[$params['type']];
+
+               $res = array();
+
+               if ( $this->getUser()->matchEditToken( $token, $salt, $request, $maxage ) ) {
+                       $res['result'] = 'valid';
+               } elseif ( $maxage !== null && $this->getUser()->matchEditToken( $token, $salt, $request ) ) {
+                       $res['result'] = 'expired';
+               } else {
+                       $res['result'] = 'invalid';
+               }
+
+               $ts = User::getEditTokenTimestamp( $token );
+               if ( $ts !== null ) {
+                       $mwts = new MWTimestamp();
+                       $mwts->timestamp->setTimestamp( $ts );
+                       $res['generated'] = $mwts->getTimestamp( TS_ISO_8601 );
+               }
+
+               $this->getResult()->addValue( null, $this->getModuleName(), $res );
+       }
+
+       public function getAllowedParams() {
+               return array(
+                       'type' => array(
+                               ApiBase::PARAM_TYPE => array_keys( ApiQueryTokens::getTokenTypeSalts() ),
+                               ApiBase::PARAM_REQUIRED => true,
+                       ),
+                       'token' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true,
+                       ),
+                       'maxtokenage' => array(
+                               ApiBase::PARAM_TYPE => 'integer',
+                       ),
+               );
+       }
+
+       protected function getExamplesMessages() {
+               return array(
+                       'action=checktoken&type=csrf&token=123ABC'
+                               => 'apihelp-checktoken-example-simple',
+               );
+       }
+}
index f17b874..34ed523 100644 (file)
@@ -64,6 +64,7 @@ class ApiMain extends ApiBase {
                'rsd' => 'ApiRsd',
                'compare' => 'ApiComparePages',
                'tokens' => 'ApiTokens',
+               'checktoken' => 'ApiCheckToken',
 
                // Write modules
                'purge' => 'ApiPurge',
index 05a1a15..02c88c4 100644 (file)
@@ -48,6 +48,8 @@ class ApiQueryInfo extends ApiQueryBase {
 
        private $tokenFunctions;
 
+       private $countTestedActions = 0;
+
        public function __construct( ApiQuery $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'in' );
        }
@@ -357,7 +359,7 @@ class ApiQueryInfo extends ApiQueryBase {
                /** @var $title Title */
                foreach ( $this->everything as $pageid => $title ) {
                        $pageInfo = $this->extractPageInfo( $pageid, $title );
-                       $fit = $result->addValue( array(
+                       $fit = $pageInfo !== null && $result->addValue( array(
                                'query',
                                'pages'
                        ), $pageid, $pageInfo );
@@ -374,7 +376,7 @@ class ApiQueryInfo extends ApiQueryBase {
         * Get a result array with information about a title
         * @param int $pageid Page ID (negative for missing titles)
         * @param Title $title
-        * @return array
+        * @return array|null
         */
        private function extractPageInfo( $pageid, $title ) {
                $pageInfo = array();
@@ -484,6 +486,22 @@ class ApiQueryInfo extends ApiQueryBase {
                        }
                }
 
+               if ( $this->params['testactions'] ) {
+                       $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML1 : self::LIMIT_SML2;
+                       if ( $this->countTestedActions >= $limit ) {
+                               return null; // force a continuation
+                       }
+
+                       $user = $this->getUser();
+                       $pageInfo['actions'] = array();
+                       foreach ( $this->params['testactions'] as $action ) {
+                               $this->countTestedActions++;
+                               if ( $title->userCan( $action, $user ) ) {
+                                       $pageInfo['actions'][$action] = '';
+                               }
+                       }
+               }
+
                return $pageInfo;
        }
 
@@ -825,6 +843,10 @@ class ApiQueryInfo extends ApiQueryBase {
                                ),
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => array(),
                        ),
+                       'testactions' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_ISMULTI => true,
+                       ),
                        'token' => array(
                                ApiBase::PARAM_DEPRECATED => true,
                                ApiBase::PARAM_DFLT => null,
index 6779571..4237ff8 100644 (file)
        "apihelp-block-example-ip-simple": "Block IP address <kbd>192.0.2.5</kbd> for three days with reason <kbd>First strike</kbd>.",
        "apihelp-block-example-user-complex": "Block user <kbd>Vandal</kbd> indefinitely with reason <kbd>Vandalism</kbd>, and prevent new account creation and email sending.",
 
+       "apihelp-checktoken-description": "Check the validity of a token from <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
+       "apihelp-checktoken-param-type": "Type of token being tested.",
+       "apihelp-checktoken-param-token": "Token to test.",
+       "apihelp-checktoken-param-maxtokenage": "Maximum allowed age of the token, in seconds.",
+       "apihelp-checktoken-example-simple": "Test the validity of a <kbd>csrf</kbd> token.",
+
        "apihelp-clearhasmsg-description": "Clears the <code>hasmsg</code> flag for the current user.",
        "apihelp-clearhasmsg-example-1": "Clear the <code>hasmsg</code> flag for the current user.",
 
        "apihelp-query+info-paramvalue-prop-readable": "Whether the user can read this page.",
        "apihelp-query+info-paramvalue-prop-preload": "Gives the text returned by EditFormPreloadText.",
        "apihelp-query+info-paramvalue-prop-displaytitle": "Gives the way the page title is actually displayed.",
+       "apihelp-query+info-param-testactions": "Test whether the current user can perform certain actions on the page.",
        "apihelp-query+info-param-token": "Use [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] instead.",
        "apihelp-query+info-example-simple": "Get information about the page <kbd>Main Page</kbd>.",
        "apihelp-query+info-example-protection": "Get general and protection information about the page <kbd>Main Page</kbd>.",
index f539ac6..3d1f3c4 100644 (file)
        "apihelp-block-param-watchuser": "{{doc-apihelp-param|block|watchuser}}",
        "apihelp-block-example-ip-simple": "{{doc-apihelp-example|block}}",
        "apihelp-block-example-user-complex": "{{doc-apihelp-example|block}}",
+       "apihelp-checktoken-description": "{{doc-apihelp-description|checktoken}}",
+       "apihelp-checktoken-param-type": "{{doc-apihelp-param|checktoken|type}}",
+       "apihelp-checktoken-param-token": "{{doc-apihelp-param|checktoken|token}}",
+       "apihelp-checktoken-param-maxtokenage": "{{doc-apihelp-param|checktoken|maxtokenage}}",
+       "apihelp-checktoken-example-simple": "{{doc-apihelp-example|checktoken}}",
        "apihelp-clearhasmsg-description": "{{doc-apihelp-description|clear<code>hasmsg</code>}}",
        "apihelp-clearhasmsg-example-1": "{{doc-apihelp-example|clear<code>hasmsg</code>}}",
        "apihelp-compare-description": "{{doc-apihelp-description|compare}}",
        "apihelp-query+info-paramvalue-prop-readable": "{{doc-apihelp-paramvalue|query+info|prop|readable}}",
        "apihelp-query+info-paramvalue-prop-preload": "{{doc-apihelp-paramvalue|query+info|prop|preload}}",
        "apihelp-query+info-paramvalue-prop-displaytitle": "{{doc-apihelp-paramvalue|query+info|prop|displaytitle}}",
+       "apihelp-query+info-param-testactions": "{{doc-apihelp-param|query+info|testactions}}",
        "apihelp-query+info-param-token": "{{doc-apihelp-param|query+info|token}}",
        "apihelp-query+info-example-simple": "{{doc-apihelp-example|query+info}}",
        "apihelp-query+info-example-protection": "{{doc-apihelp-example|query+info}}",