Add support for an anonymous OAuth consumer. Note: this requires a

small DB tweak.  Oauth_application_user needs to have the primary
compound key: (profile_id, application_id, token).

http://status.net/open-source/issues/2761

This should also make it possible to have multiple access tokens
per application.

http://status.net/open-source/issues/2788
This commit is contained in:
Zach Copley 2010-10-19 20:54:53 -07:00
parent d48f4a81d6
commit e8b6d7c946
11 changed files with 338 additions and 142 deletions

View File

@ -81,7 +81,7 @@ class ApiOauthAccessTokenAction extends ApiOauthAction
$app = $datastore->getAppByRequestToken($this->reqToken); $app = $datastore->getAppByRequestToken($this->reqToken);
$atok = $server->fetch_access_token($req); $atok = $server->fetch_access_token($req);
} catch (OAuthException $e) { } catch (Exception $e) {
common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage()); common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage());
common_debug(var_export($req, true)); common_debug(var_export($req, true));
$code = $e->getCode(); $code = $e->getCode();
@ -99,7 +99,7 @@ class ApiOauthAccessTokenAction extends ApiOauthAction
$this->verifier $this->verifier
); );
common_log(LOG_WARNIGN, $msg); common_log(LOG_WARNING, $msg);
$this->clientError(_("Invalid request token or verifier.", 400, 'text')); $this->clientError(_("Invalid request token or verifier.", 400, 'text'));
} else { } else {

View File

@ -177,21 +177,6 @@ class ApiOauthAuthorizeAction extends Action
$this->serverError($e->getMessage()); $this->serverError($e->getMessage());
} }
// Check to see if there was a previous token associated
// with this user/app and kill it. If the user is doing this she
// probably doesn't want any old tokens anyway.
$appUser = Oauth_application_user::getByKeys($user, $this->app);
if (!empty($appUser)) {
$result = $appUser->delete();
if (!$result) {
common_log_db_error($appUser, 'DELETE', __FILE__);
$this->serverError(_('Database error deleting OAuth application user.'));
}
}
// associated the authorized req token with the user and the app // associated the authorized req token with the user and the app
$appUser = new Oauth_application_user(); $appUser = new Oauth_application_user();

View File

@ -150,7 +150,6 @@ require_once INSTALLDIR . '/lib/mediafile.php';
class ApiStatusesUpdateAction extends ApiAuthAction class ApiStatusesUpdateAction extends ApiAuthAction
{ {
var $source = null;
var $status = null; var $status = null;
var $in_reply_to_status_id = null; var $in_reply_to_status_id = null;
var $lat = null; var $lat = null;

View File

@ -22,7 +22,7 @@
* @category Settings * @category Settings
* @package StatusNet * @package StatusNet
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>
* @copyright 2008-2009 StatusNet, Inc. * @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/ * @link http://status.net/
*/ */
@ -50,13 +50,13 @@ require_once INSTALLDIR . '/lib/apioauthstore.php';
class OauthconnectionssettingsAction extends ConnectSettingsAction class OauthconnectionssettingsAction extends ConnectSettingsAction
{ {
var $page = null; var $page = null;
var $id = null; var $oauth_token = null;
function prepare($args) function prepare($args)
{ {
parent::prepare($args); parent::prepare($args);
$this->id = (int)$this->arg('id'); $this->oauth_token = $this->arg('oauth_token');
$this->page = ($this->arg('page')) ? ($this->arg('page') + 0) : 1; $this->page = ($this->arg('page')) ? ($this->arg('page') + 0) : 1;
return true; return true;
} }
@ -80,7 +80,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
function getInstructions() function getInstructions()
{ {
return _('You have allowed the following applications to access your account.'); return _('The following connections exist for your account.');
} }
/** /**
@ -97,22 +97,26 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
$offset = ($this->page - 1) * APPS_PER_PAGE; $offset = ($this->page - 1) * APPS_PER_PAGE;
$limit = APPS_PER_PAGE + 1; $limit = APPS_PER_PAGE + 1;
$application = $profile->getApplications($offset, $limit); $connection = $profile->getConnectedApps($offset, $limit);
$cnt = 0; $cnt = 0;
if (!empty($application)) { if (!empty($connection)) {
$al = new ApplicationList($application, $user, $this, true); $cal = new ConnectedAppsList($connection, $user, $this);
$cnt = $al->show(); $cnt = $cal->show();
} }
if ($cnt == 0) { if ($cnt == 0) {
$this->showEmptyListMessage(); $this->showEmptyListMessage();
} }
$this->pagination($this->page > 1, $cnt > APPS_PER_PAGE, $this->pagination(
$this->page, 'connectionssettings', $this->page > 1,
array('nickname' => $user->nickname)); $cnt > APPS_PER_PAGE,
$this->page,
'connectionssettings',
array('nickname' => $user->nickname)
);
} }
/** /**
@ -138,11 +142,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
} }
if ($this->arg('revoke')) { if ($this->arg('revoke')) {
$this->revokeAccess($this->id); $this->revokeAccess($this->oauth_token);
// XXX: Show some indicator to the user of what's been done.
$this->showPage();
} else { } else {
$this->clientError(_('Unexpected form submission.'), 401); $this->clientError(_('Unexpected form submission.'), 401);
return false; return false;
@ -150,32 +150,27 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
} }
/** /**
* Revoke access to an authorized OAuth application * Revoke an access token
*
* XXX: Confirm revoke before doing it
* *
* @param int $appId the ID of the application * @param int $appId the ID of the application
* *
*/ */
function revokeAccess($appId) function revokeAccess($token)
{ {
$cur = common_current_user(); $cur = common_current_user();
$app = Oauth_application::staticGet('id', $appId); $appUser = Oauth_application_user::getByUserAndToken($cur, $token);
if (empty($app)) {
$this->clientError(_('No such application.'), 404);
return false;
}
// XXX: Transaction here?
$appUser = Oauth_application_user::getByKeys($cur, $app);
if (empty($appUser)) { if (empty($appUser)) {
$this->clientError(_('You are not a user of that application.'), 401); $this->clientError(_('You are not a user of that application.'), 401);
return false; return false;
} }
$app = Oauth_application::staticGet('id', $appUser->application_id);
$datastore = new ApiStatusNetOAuthDataStore(); $datastore = new ApiStatusNetOAuthDataStore();
$datastore->revoke_token($appUser->token, 1); $datastore->revoke_token($appUser->token, 1);
@ -187,10 +182,25 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
return false; return false;
} }
$msg = 'User %s (id: %d) revoked access to app %s (id: %d)'; $msg = 'API OAuth - user %s (id: %d) revoked access token %s for app id %d';
common_log(LOG_INFO, sprintf($msg, $cur->nickname, common_log(
$cur->id, $app->name, $app->id)); LOG_INFO,
sprintf(
$msg,
$cur->nickname,
$cur->id,
$appUser->token,
$appUser->application_id
)
);
$msg = sprintf(
_('You have successfully revoked access for %s and the access token starting with %s'),
$app->name,
substr($appUser->token, 0, 7)
);
$this->showForm($msg, true);
} }
function showEmptyListMessage() function showEmptyListMessage()
@ -204,15 +214,20 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
function showSections() function showSections()
{ {
$cur = common_current_user(); $cur = common_current_user();
$this->element('h2', null, 'Developers'); $this->element('h2', null, 'Developers');
$this->elementStart('p'); $this->elementStart('p');
$this->raw(_('Developers can edit the registration settings for their applications '));
$this->element('a', $devMsg = sprintf(
array('href' => common_local_url('oauthappssettings')), _('Are you a developer? [Register an OAuth client application](%s) to use with this instance of StatusNet.'),
'here.'); common_local_url('oauthappssettings')
$this->elementEnd('p'); );
$output = common_markup_to_html($devMsg);
$this->raw($output);
$this->elementEnd('p');
} }
} }

View File

@ -13,7 +13,7 @@ class Oauth_application_user extends Memcached_DataObject
public $profile_id; // int(4) primary_key not_null public $profile_id; // int(4) primary_key not_null
public $application_id; // int(4) primary_key not_null public $application_id; // int(4) primary_key not_null
public $access_type; // tinyint(1) public $access_type; // tinyint(1)
public $token; // varchar(255) public $token; // varchar(255) primary_key not_null
public $created; // datetime not_null public $created; // datetime not_null
public $modified; // timestamp not_null default_CURRENT_TIMESTAMP public $modified; // timestamp not_null default_CURRENT_TIMESTAMP
@ -24,20 +24,51 @@ class Oauth_application_user extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###END_AUTOCODE
static function getByKeys($user, $app) static function getByUserAndToken($user, $token)
{ {
if (empty($user) || empty($app)) { if (empty($user) || empty($token)) {
return null; return null;
} }
$oau = new Oauth_application_user(); $oau = new Oauth_application_user();
$oau->profile_id = $user->id; $oau->profile_id = $user->id;
$oau->application_id = $app->id; $oau->token = $token;
$oau->limit(1); $oau->limit(1);
$result = $oau->find(true); $result = $oau->find(true);
return empty($result) ? null : $oau; return empty($result) ? null : $oau;
} }
function updateKeys(&$orig)
{
$this->_connect();
$parts = array();
foreach (array('profile_id', 'application_id', 'token', 'access_type') as $k) {
if (strcmp($this->$k, $orig->$k) != 0) {
$parts[] = $k . ' = ' . $this->_quote($this->$k);
}
}
if (count($parts) == 0) {
# No changes
return true;
}
$toupdate = implode(', ', $parts);
$table = $this->tableName();
if(common_config('db','quote_identifiers')) {
$table = '"' . $table . '"';
}
$qry = 'UPDATE ' . $table . ' SET ' . $toupdate .
' WHERE profile_id = ' . $orig->profile_id
. ' AND application_id = ' . $orig->application_id
. " AND token = '$orig->token'";
$orig->decache();
$result = $this->query($qry);
if ($result) {
$this->encache();
}
return $result;
}
} }

View File

@ -401,10 +401,10 @@ class Profile extends Memcached_DataObject
return $profile; return $profile;
} }
function getApplications($offset = 0, $limit = null) function getConnectedApps($offset = 0, $limit = null)
{ {
$qry = $qry =
'SELECT a.* ' . 'SELECT u.* ' .
'FROM oauth_application_user u, oauth_application a ' . 'FROM oauth_application_user u, oauth_application a ' .
'WHERE u.profile_id = %d ' . 'WHERE u.profile_id = %d ' .
'AND a.id = u.application_id ' . 'AND a.id = u.application_id ' .
@ -419,11 +419,11 @@ class Profile extends Memcached_DataObject
} }
} }
$application = new Oauth_application(); $apps = new Oauth_application_user();
$cnt = $application->query(sprintf($qry, $this->id)); $cnt = $apps->query(sprintf($qry, $this->id));
return $application; return $apps;
} }
function subscriptionCount() function subscriptionCount()

View File

@ -393,13 +393,14 @@ name = U
profile_id = 129 profile_id = 129
application_id = 129 application_id = 129
access_type = 17 access_type = 17
token = 2 token = 130
created = 142 created = 142
modified = 384 modified = 384
[oauth_application_user__keys] [oauth_application_user__keys]
profile_id = K profile_id = K
application_id = K application_id = K
token = K
[profile] [profile]
id = 129 id = 129

View File

@ -231,10 +231,10 @@ create table oauth_application_user (
profile_id integer not null comment 'user of the application' references profile (id), profile_id integer not null comment 'user of the application' references profile (id),
application_id integer not null comment 'id of the application' references oauth_application (id), application_id integer not null comment 'id of the application' references oauth_application (id),
access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write', access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write',
token varchar(255) comment 'request or access token', token varchar(255) not null comment 'request or access token',
created datetime not null comment 'date this record was created', created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified', modified timestamp comment 'date this record was modified',
constraint primary key (profile_id, application_id) constraint primary key (profile_id, application_id, token)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
/* These are used by JanRain OpenID library */ /* These are used by JanRain OpenID library */

View File

@ -178,8 +178,10 @@ class ApiAuthAction extends ApiAction
} }
// set the source attr // set the source attr
if ($app->name != 'anonymous') {
$this->source = $app->name;
}
$this->source = $app->name;
$appUser = Oauth_application_user::staticGet('token', $access_token); $appUser = Oauth_application_user::staticGet('token', $access_token);

View File

@ -23,16 +23,43 @@ require_once INSTALLDIR . '/lib/oauthstore.php';
class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
{ {
function lookup_consumer($consumer_key) function lookup_consumer($consumerKey)
{ {
$con = Consumer::staticGet('consumer_key', $consumer_key); $con = Consumer::staticGet('consumer_key', $consumerKey);
if (!$con) { if (!$con) {
return null;
// Create an anon consumer and anon application if one
// doesn't exist already
if ($consumerKey == 'anonymous') {
$con = new Consumer();
$con->consumer_key = $consumerKey;
$con->consumer_secret = $consumerKey;
$result = $con->insert();
if (!$result) {
$this->serverError(_("Could not create anonymous consumer."));
}
$app = new OAuth_application();
$app->consumer_key = $con->consumer_key;
$app->name = 'anonymous';
// XXX: allow the user to set the access type when
// authorizing? Currently we default to r+w for anonymous
// OAuth client applications
$app->access_type = 3; // read + write
$id = $app->insert();
if (!$id) {
$this->serverError(_("Could not create anonymous OAuth application."));
}
} else {
return null;
}
} }
return new OAuthConsumer($con->consumer_key, return new OAuthConsumer(
$con->consumer_secret); $con->consumer_key,
$con->consumer_secret
);
} }
function getAppByRequestToken($token_key) function getAppByRequestToken($token_key)
@ -94,7 +121,7 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
if ($rt->find(true) && $rt->state == 1 && $rt->verifier == $verifier) { // authorized if ($rt->find(true) && $rt->state == 1 && $rt->verifier == $verifier) { // authorized
common_debug('request token found.', __FILE__); common_debug('request token found.');
// find the associated user of the app // find the associated user of the app
@ -140,6 +167,7 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
// update the token from req to access for the user // update the token from req to access for the user
$orig = clone($appUser); $orig = clone($appUser);
$appUser->token = $at->tok; $appUser->token = $at->tok;
// It's at this point that we change the access type // It's at this point that we change the access type
@ -150,11 +178,10 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
$appUser->access_type = $app->access_type; $appUser->access_type = $app->access_type;
$result = $appUser->update($orig); $result = $appUser->updateKeys($orig);
if (empty($result)) { if (!$result) {
common_debug('couldn\'t update OAuth app user.'); throw new Exception('Couldn\'t update OAuth app user.');
return null;
} }
// Okay, good // Okay, good
@ -179,9 +206,9 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
* @return void * @return void
*/ */
public function revoke_token($token_key, $type = 0) { public function revoke_token($token_key, $type = 0) {
$rt = new Token(); $rt = new Token();
$rt->tok = $token_key; $rt->tok = $token_key;
$rt->type = $type; $rt->type = $type;
$rt->state = 0; $rt->state = 0;
if (!$rt->find(true)) { if (!$rt->find(true)) {

View File

@ -22,7 +22,7 @@
* @category Application * @category Application
* @package StatusNet * @package StatusNet
* @author Zach Copley <zach@status.net> * @author Zach Copley <zach@status.net>
* @copyright 2008-2009 StatusNet, Inc. * @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/ * @link http://status.net/
*/ */
@ -55,14 +55,13 @@ class ApplicationList extends Widget
/** Action object using us. */ /** Action object using us. */
var $action = null; var $action = null;
function __construct($application, $owner=null, $action=null, $connections = false) function __construct($application, $owner=null, $action=null)
{ {
parent::__construct($action); parent::__construct($action);
$this->application = $application; $this->application = $application;
$this->owner = $owner; $this->owner = $owner;
$this->action = $action; $this->action = $action;
$this->connections = $connections;
} }
function show() function show()
@ -88,24 +87,34 @@ class ApplicationList extends Widget
{ {
$user = common_current_user(); $user = common_current_user();
$this->out->elementStart('li', array('class' => 'application', $this->out->elementStart(
'id' => 'oauthclient-' . $this->application->id)); 'li',
array(
'class' => 'application',
'id' => 'oauthclient-' . $this->application->id
)
);
$this->out->elementStart('span', 'vcard author'); $this->out->elementStart('span', 'vcard author');
if (!$this->connections) {
$this->out->elementStart('a',
array('href' => common_local_url('showapplication',
array('id' => $this->application->id)),
'class' => 'url'));
} else { $this->out->elementStart(
$this->out->elementStart('a', array('href' => $this->application->source_url, 'a',
'class' => 'url')); array(
} 'href' => common_local_url(
'showapplication',
array('id' => $this->application->id)),
'class' => 'url'
)
);
if (!empty($this->application->icon)) { if (!empty($this->application->icon)) {
$this->out->element('img', array('src' => $this->application->icon, $this->out->element(
'class' => 'photo avatar')); 'img',
array(
'src' => $this->application->icon,
'class' => 'photo avatar'
)
);
} }
$this->out->element('span', 'fn', $this->application->name); $this->out->element('span', 'fn', $this->application->name);
@ -114,51 +123,18 @@ class ApplicationList extends Widget
$this->out->raw(' by '); $this->out->raw(' by ');
$this->out->element('a', array('href' => $this->application->homepage, $this->out->element(
'class' => 'url'), 'a',
$this->application->organization); array(
'href' => $this->application->homepage,
'class' => 'url'
),
$this->application->organization
);
$this->out->element('p', 'note', $this->application->description); $this->out->element('p', 'note', $this->application->description);
$this->out->elementEnd('li'); $this->out->elementEnd('li');
if ($this->connections) {
$appUser = Oauth_application_user::getByKeys($this->owner, $this->application);
if (empty($appUser)) {
common_debug("empty appUser!");
}
$this->out->elementStart('li');
// TRANS: Application access type
$readWriteText = _('read-write');
// TRANS: Application access type
$readOnlyText = _('read-only');
$access = ($this->application->access_type & Oauth_application::$writeAccess)
? $readWriteText : $readOnlyText;
$modifiedDate = common_date_string($appUser->modified);
// TRANS: Used in application list. %1$s is a modified date, %2$s is access type ("read-write" or "read-only")
$txt = sprintf(_('Approved %1$s - "%2$s" access.'),$modifiedDate,$access);
$this->out->raw($txt);
$this->out->elementEnd('li');
$this->out->elementStart('li', 'entity_revoke');
$this->out->elementStart('form', array('id' => 'form_revoke_app',
'class' => 'form_revoke_app',
'method' => 'POST',
'action' =>
common_local_url('oauthconnectionssettings')));
$this->out->elementStart('fieldset');
$this->out->hidden('id', $this->application->id);
$this->out->hidden('token', common_session_token());
// TRANS: Button label
$this->out->submit('revoke', _m('BUTTON','Revoke'));
$this->out->elementEnd('fieldset');
$this->out->elementEnd('form');
$this->out->elementEnd('li');
}
} }
/* Override this in subclasses. */ /* Override this in subclasses. */
@ -166,4 +142,164 @@ class ApplicationList extends Widget
{ {
return; return;
} }
}
/**
* Widget to show a list of connected OAuth clients
*
* @category Application
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class ConnectedAppsList extends Widget
{
/** Current connected application query */
var $connection = null;
/** Owner of this list */
var $owner = null;
/** Action object using us. */
var $action = null;
function __construct($connection, $owner=null, $action=null)
{
parent::__construct($action);
common_debug("ConnectedAppsList constructor");
$this->connection = $connection;
$this->owner = $owner;
$this->action = $action;
}
/* Override this in subclasses. */
function showOwnerControls()
{
return;
}
function show()
{
$this->out->elementStart('ul', 'applications');
$cnt = 0;
while ($this->connection->fetch()) {
$cnt++;
if($cnt > APPS_PER_PAGE) {
break;
}
$this->showConnection();
}
$this->out->elementEnd('ul');
return $cnt;
}
function showConnection()
{
$app = Oauth_application::staticGet('id', $this->connection->application_id);
$this->out->elementStart(
'li',
array(
'class' => 'application',
'id' => 'oauthclient-' . $app->id
)
);
$this->out->elementStart('span', 'vcard author');
$this->out->elementStart(
'a',
array(
'href' => $app->source_url,
'class' => 'url'
)
);
if (!empty($app->icon)) {
$this->out->element(
'img',
array(
'src' => $app->icon,
'class' => 'photo avatar'
)
);
}
if ($app->name != 'anonymous') {
$this->out->element('span', 'fn', $app->name);
}
$this->out->elementEnd('a');
if ($app->name == 'anonymous') {
$this->out->element('span', 'fn', "Unknown application");
}
$this->out->elementEnd('span');
if ($app->name != 'anonymous') {
$this->out->raw(_(' by '));
$this->out->element(
'a',
array(
'href' => $app->homepage,
'class' => 'url'
),
$app->organization
);
}
// TRANS: Application access type
$readWriteText = _('read-write');
// TRANS: Application access type
$readOnlyText = _('read-only');
$access = ($this->connection->access_type & Oauth_application::$writeAccess)
? $readWriteText : $readOnlyText;
$modifiedDate = common_date_string($this->connection->modified);
// TRANS: Used in application list. %1$s is a modified date, %2$s is access type ("read-write" or "read-only")
$txt = sprintf(_('Approved %1$s - "%2$s" access.'), $modifiedDate, $access);
$this->out->raw(" - $txt");
if (!empty($app->description)) {
$this->out->element(
'p', array('class' => 'application_description'),
$app->description
);
}
$this->out->element(
'p', array(
'class' => 'access_token'),
_('Access token starting with: ') . substr($this->connection->token, 0, 7)
);
$this->out->elementStart(
'form',
array(
'id' => 'form_revoke_app',
'class' => 'form_revoke_app',
'method' => 'POST',
'action' => common_local_url('oauthconnectionssettings')
)
);
$this->out->elementStart('fieldset');
$this->out->hidden('oauth_token', $this->connection->token);
$this->out->hidden('token', common_session_token());
// TRANS: Button label
$this->out->submit('revoke', _('Revoke'));
$this->out->elementEnd('fieldset');
$this->out->elementEnd('form');
$this->out->elementEnd('li');
}
} }