From 4b8b0358ebca88abe8aa933becc5fd917d1799f4 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Thu, 29 Jan 2015 12:14:40 -0800 Subject: [PATCH] API: Add authz features for RESTBase 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 | 3 ++ autoload.php | 1 + includes/User.php | 20 +++++++-- includes/api/ApiCheckToken.php | 81 ++++++++++++++++++++++++++++++++++ includes/api/ApiMain.php | 1 + includes/api/ApiQueryInfo.php | 26 ++++++++++- includes/api/i18n/en.json | 7 +++ includes/api/i18n/qqq.json | 6 +++ 8 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 includes/api/ApiCheckToken.php diff --git a/RELEASE-NOTES-1.25 b/RELEASE-NOTES-1.25 index 8992ce0ce9..86395ac21e 100644 --- a/RELEASE-NOTES-1.25 +++ b/RELEASE-NOTES-1.25 @@ -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. diff --git a/autoload.php b/autoload.php index 01dba44784..bf759bc66a 100644 --- a/autoload.php +++ b/autoload.php @@ -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', diff --git a/includes/User.php b/includes/User.php index c2db67a2a7..ae8deb602a 100644 --- a/includes/User.php +++ b/includes/User.php @@ -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 index 0000000000..28c6ece7c0 --- /dev/null +++ b/includes/api/ApiCheckToken.php @@ -0,0 +1,81 @@ +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', + ); + } +} diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index f17b8741bb..34ed523031 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -64,6 +64,7 @@ class ApiMain extends ApiBase { 'rsd' => 'ApiRsd', 'compare' => 'ApiComparePages', 'tokens' => 'ApiTokens', + 'checktoken' => 'ApiCheckToken', // Write modules 'purge' => 'ApiPurge', diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 05a1a15604..02c88c4fc6 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -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, diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 677957100b..4237ff8ea5 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -34,6 +34,12 @@ "apihelp-block-example-ip-simple": "Block IP address 192.0.2.5 for three days with reason First strike.", "apihelp-block-example-user-complex": "Block user Vandal indefinitely with reason Vandalism, and prevent new account creation and email sending.", + "apihelp-checktoken-description": "Check the validity of a token from [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", + "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 csrf token.", + "apihelp-clearhasmsg-description": "Clears the hasmsg flag for the current user.", "apihelp-clearhasmsg-example-1": "Clear the hasmsg flag for the current user.", @@ -691,6 +697,7 @@ "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 Main Page.", "apihelp-query+info-example-protection": "Get general and protection information about the page Main Page.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index f539ac6a53..3d1f3c46fb 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -35,6 +35,11 @@ "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|clearhasmsg}}", "apihelp-clearhasmsg-example-1": "{{doc-apihelp-example|clearhasmsg}}", "apihelp-compare-description": "{{doc-apihelp-description|compare}}", @@ -636,6 +641,7 @@ "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}}", -- 2.20.1