Commit e8b6d7c9 authored by Zach Copley's avatar Zach Copley

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
parent d48f4a81
......@@ -81,7 +81,7 @@ class ApiOauthAccessTokenAction extends ApiOauthAction
$app = $datastore->getAppByRequestToken($this->reqToken);
$atok = $server->fetch_access_token($req);
} catch (OAuthException $e) {
} catch (Exception $e) {
common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage());
common_debug(var_export($req, true));
$code = $e->getCode();
......@@ -99,7 +99,7 @@ class ApiOauthAccessTokenAction extends ApiOauthAction
$this->verifier
);
common_log(LOG_WARNIGN, $msg);
common_log(LOG_WARNING, $msg);
$this->clientError(_("Invalid request token or verifier.", 400, 'text'));
} else {
......
......@@ -177,21 +177,6 @@ class ApiOauthAuthorizeAction extends Action
$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
$appUser = new Oauth_application_user();
......
......@@ -150,7 +150,6 @@ require_once INSTALLDIR . '/lib/mediafile.php';
class ApiStatusesUpdateAction extends ApiAuthAction
{
var $source = null;
var $status = null;
var $in_reply_to_status_id = null;
var $lat = null;
......
......@@ -22,7 +22,7 @@
* @category Settings
* @package StatusNet
* @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
* @link http://status.net/
*/
......@@ -50,13 +50,13 @@ require_once INSTALLDIR . '/lib/apioauthstore.php';
class OauthconnectionssettingsAction extends ConnectSettingsAction
{
var $page = null;
var $id = null;
var $page = null;
var $oauth_token = null;
function 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;
return true;
}
......@@ -80,7 +80,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
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;
$limit = APPS_PER_PAGE + 1;
$application = $profile->getApplications($offset, $limit);
$connection = $profile->getConnectedApps($offset, $limit);
$cnt = 0;
if (!empty($application)) {
$al = new ApplicationList($application, $user, $this, true);
$cnt = $al->show();
if (!empty($connection)) {
$cal = new ConnectedAppsList($connection, $user, $this);
$cnt = $cal->show();
}
if ($cnt == 0) {
$this->showEmptyListMessage();
}
$this->pagination($this->page > 1, $cnt > APPS_PER_PAGE,
$this->page, 'connectionssettings',
array('nickname' => $user->nickname));
$this->pagination(
$this->page > 1,
$cnt > APPS_PER_PAGE,
$this->page,
'connectionssettings',
array('nickname' => $user->nickname)
);
}
/**
......@@ -138,11 +142,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
}
if ($this->arg('revoke')) {
$this->revokeAccess($this->id);
// XXX: Show some indicator to the user of what's been done.
$this->showPage();
$this->revokeAccess($this->oauth_token);
} else {
$this->clientError(_('Unexpected form submission.'), 401);
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
*
*/
function revokeAccess($appId)
function revokeAccess($token)
{
$cur = common_current_user();
$app = Oauth_application::staticGet('id', $appId);
if (empty($app)) {
$this->clientError(_('No such application.'), 404);
return false;
}
// XXX: Transaction here?
$appUser = Oauth_application_user::getByKeys($cur, $app);
$appUser = Oauth_application_user::getByUserAndToken($cur, $token);
if (empty($appUser)) {
$this->clientError(_('You are not a user of that application.'), 401);
return false;
}
$app = Oauth_application::staticGet('id', $appUser->application_id);
$datastore = new ApiStatusNetOAuthDataStore();
$datastore->revoke_token($appUser->token, 1);
......@@ -187,10 +182,25 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
return false;
}
$msg = 'User %s (id: %d) revoked access to app %s (id: %d)';
common_log(LOG_INFO, sprintf($msg, $cur->nickname,
$cur->id, $app->name, $app->id));
$msg = 'API OAuth - user %s (id: %d) revoked access token %s for app id %d';
common_log(
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()
......@@ -204,15 +214,20 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
function showSections()
{
$cur = common_current_user();
$this->element('h2', null, 'Developers');
$this->elementStart('p');
$this->raw(_('Developers can edit the registration settings for their applications '));
$this->element('a',
array('href' => common_local_url('oauthappssettings')),
'here.');
$this->elementEnd('p');
$cur = common_current_user();
$this->element('h2', null, 'Developers');
$this->elementStart('p');
$devMsg = sprintf(
_('Are you a developer? [Register an OAuth client application](%s) to use with this instance of StatusNet.'),
common_local_url('oauthappssettings')
);
$output = common_markup_to_html($devMsg);
$this->raw($output);
$this->elementEnd('p');
}
}
......@@ -13,7 +13,7 @@ class Oauth_application_user extends Memcached_DataObject
public $profile_id; // int(4) primary_key not_null
public $application_id; // int(4) primary_key not_null
public $access_type; // tinyint(1)
public $token; // varchar(255)
public $token; // varchar(255) primary_key not_null
public $created; // datetime not_null
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 */
###END_AUTOCODE
static function getByKeys($user, $app)
static function getByUserAndToken($user, $token)
{
if (empty($user) || empty($app)) {
if (empty($user) || empty($token)) {
return null;
}
$oau = new Oauth_application_user();
$oau->profile_id = $user->id;
$oau->application_id = $app->id;
$oau->profile_id = $user->id;
$oau->token = $token;
$oau->limit(1);
$result = $oau->find(true);
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;
}
}
......@@ -401,10 +401,10 @@ class Profile extends Memcached_DataObject
return $profile;
}
function getApplications($offset = 0, $limit = null)
function getConnectedApps($offset = 0, $limit = null)
{
$qry =
'SELECT a.* ' .
'SELECT u.* ' .
'FROM oauth_application_user u, oauth_application a ' .
'WHERE u.profile_id = %d ' .
'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()
......
......@@ -393,13 +393,14 @@ name = U
profile_id = 129
application_id = 129
access_type = 17
token = 2
token = 130
created = 142
modified = 384
[oauth_application_user__keys]
profile_id = K
application_id = K
token = K
[profile]
id = 129
......
......@@ -231,10 +231,10 @@ create table oauth_application_user (
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),
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',
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;
/* These are used by JanRain OpenID library */
......
......@@ -178,8 +178,10 @@ class ApiAuthAction extends ApiAction
}
// set the source attr
if ($app->name != 'anonymous') {
$this->source = $app->name;
}
$this->source = $app->name;
$appUser = Oauth_application_user::staticGet('token', $access_token);
......
......@@ -23,16 +23,43 @@ require_once INSTALLDIR . '/lib/oauthstore.php';
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) {
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,
$con->consumer_secret);
return new OAuthConsumer(
$con->consumer_key,
$con->consumer_secret
);
}
function getAppByRequestToken($token_key)
......@@ -94,7 +121,7 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
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
......@@ -140,6 +167,7 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
// update the token from req to access for the user
$orig = clone($appUser);
$appUser->token = $at->tok;
// 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;
$result = $appUser->update($orig);
$result = $appUser->updateKeys($orig);
if (empty($result)) {
common_debug('couldn\'t update OAuth app user.');
return null;
if (!$result) {
throw new Exception('Couldn\'t update OAuth app user.');
}
// Okay, good
......@@ -179,9 +206,9 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
* @return void
*/
public function revoke_token($token_key, $type = 0) {
$rt = new Token();
$rt->tok = $token_key;
$rt->type = $type;
$rt = new Token();
$rt->tok = $token_key;
$rt->type = $type;
$rt->state = 0;
if (!$rt->find(true)) {
......
......@@ -22,7 +22,7 @@
* @category Application
* @package StatusNet
* @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
* @link http://status.net/
*/
......@@ -55,14 +55,13 @@ class ApplicationList extends Widget
/** Action object using us. */
var $action = null;
function __construct($application, $owner=null, $action=null, $connections = false)
function __construct($application, $owner=null, $action=null)
{
parent::__construct($action);
$this->application = $application;
$this->owner = $owner;
$this->action = $action;
$this->connections = $connections;
}
function show()
......@@ -88,24 +87,34 @@ class ApplicationList extends Widget
{
$user = common_current_user();
$this->out->elementStart('li', array('class' => 'application',
'id' => 'oauthclient-' . $this->application->id));
$this->out->elementStart(
'li',
array(
'class' => 'application',
'id' => 'oauthclient-' . $this->application->id
)
);
$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('a', array('href' => $this->application->source_url,
'class' => 'url'));
}
$this->out->elementStart(
'a',
array(
'href' => common_local_url(
'showapplication',
array('id' => $this->application->id)),
'class' => 'url'
)
);
if (!empty($this->application->icon)) {
$this->out->element('img', array('src' => $this->application->icon,
'class' => 'photo avatar'));
$this->out->element(
'img',
array(
'src' => $this->application->icon,
'class' => 'photo avatar'
)
);
}
$this->out->element('span', 'fn', $this->application->name);
......@@ -114,51 +123,58 @@ class ApplicationList extends Widget
$this->out->raw(' by ');
$this->out->element('a', array('href' => $this->application->homepage,
'class' => 'url'),
$this->application->organization);
$this->out->element(
'a',
array(
'href' => $this->application->homepage,
'class' => 'url'
),
$this->application->organization
);
$this->out->element('p', 'note', $this->application->description);
$this->out->elementEnd('li');
if ($this->connections) {
$appUser = Oauth_application_user::getByKeys($this->owner, $this->application);
}
if (empty($appUser)) {
common_debug("empty appUser!");
}
/* Override this in subclasses. */
function showOwnerControls()
{
return;
}
$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');
}
}
/**
* 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. */
......@@ -166,4 +182,124 @@ class ApplicationList extends Widget
{
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') {