launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06365
[Merge] lp:~rvb/maas/maas-multiple-keys2 into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/maas-multiple-keys2 into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/maas-multiple-keys2/+merge/92940
This branch changes the Account model, the API and the preferences page (http://localhost:8000/account/prefs/?tab=1) to allow a user to have multiple API tokens at his disposal.
Drive-by fixes:
- fix src/maasserver/api.py.__all__ and have the handlers included in the API documentation discovered by introspecting the module.
- fix the api display of the api doc when there is more than one operation to be described (broken display: http://91.189.93.91:8000/api/doc/)
- indentation in src/maasserver/static/js/node_add.js
--
https://code.launchpad.net/~rvb/maas/maas-multiple-keys2/+merge/92940
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-multiple-keys2 into lp:maas.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-02-13 17:07:22 +0000
+++ src/maasserver/api.py 2012-02-14 10:48:33 +0000
@@ -12,12 +12,16 @@
__all__ = [
"api_doc",
"generate_api_doc",
+ "AccountHandler",
"FilesHandler",
"NodeHandler",
+ "NodesHandler",
+ "NodeMacHandler",
"NodeMacsHandler",
]
import httplib
+import sys
import types
from django.core.exceptions import (
@@ -48,7 +52,10 @@
)
from piston.authentication import OAuthAuthentication
from piston.doc import generate_doc
-from piston.handler import BaseHandler
+from piston.handler import (
+ BaseHandler,
+ HandlerMetaClass,
+ )
from piston.utils import rc
@@ -183,7 +190,7 @@
dispatcher.__doc__ = (
"The actual operation to execute depends on the value of the '%s' "
"parameter:\n\n" % OP_PARAM)
- dispatcher.__doc__ += "\n- ".join(
+ dispatcher.__doc__ += "\n".join(
"- Operation '%s' (op=%s):\n\t%s" % (name, name, op.__doc__)
for name, op in cls._available_api_methods[method].iteritems())
@@ -392,36 +399,66 @@
return ('files_handler', [])
+@api_operations
class AccountHandler(BaseHandler):
"""Manage the current logged-in user."""
allowed_methods = ('POST',)
- @api_exported('reset_authorisation_token')
- def reset_authorisation_token(self, request):
- """Regenerate the token and the secret of the OAuth Token.
+ @api_exported('create_authorisation_token', method='POST')
+ def create_authorisation_token(self, request):
+ """Create an authorisation OAuth token and OAuth consumer.
- :return: A json dict with three keys: 'token_key',
+ :return: a json dict with three keys: 'token_key',
'token_secret' and 'consumer_key' (e.g.
{token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',
- consumer_key: '68543fhj854fg'}).
+ consumer_key: '68543fhj854fg'}).
+ :rtype: string (json)
"""
profile = request.user.get_profile()
- consumer, token = profile.reset_authorisation_token()
+ consumer, token = profile.create_authorisation_token()
return {
'token_key': token.key, 'token_secret': token.secret,
'consumer_key': consumer.key,
}
+ @api_exported('delete_authorisation_token', method='POST')
+ def delete_authorisation_token(self, request):
+ """Delete an authorisation OAuth token and the related OAuth consumer.
+
+ :param token_key: The key of the token to be deleted.
+ :type token_key: str
+
+ """
+ profile = request.user.get_profile()
+ token_key = request.data.get('token_key', None)
+ if token_key is None:
+ raise ValidationError('No provided token_key!')
+ profile.delete_authorisation_token(token_key)
+ return rc.DELETED
+
+ @classmethod
+ def resource_uri(cls, *args, **kwargs):
+ return ('account_handler', [])
+
def generate_api_doc(add_title=False):
- docs = (
- generate_doc(FilesHandler),
- generate_doc(NodesHandler),
- generate_doc(NodeHandler),
- generate_doc(NodeMacsHandler),
- generate_doc(NodeMacHandler),
- )
+ # Fetch all the API Handlers (objects with the class
+ # HandlerMetaClass).
+ modname = globals()['__name__']
+ module = sys.modules[modname]
+ handlers = [
+ getattr(module, itemname) for itemname in module.__all__
+ if getattr(module, itemname).__class__ == HandlerMetaClass
+ ]
+ # Make sure each handler defines a 'resource_uri' method (this is
+ # easily forgotten and essential to have a proper documentation).
+ for handler in handlers:
+ has_resource_uri = hasattr(handler, 'resource_uri')
+ assert has_resource_uri, "Missing resource_uri in %s" % (
+ handler.__name__)
+
+ docs = [generate_doc(handler) for handler in handlers]
messages = []
if add_title:
=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py 2012-02-13 13:07:48 +0000
+++ src/maasserver/models.py 2012-02-14 10:48:33 +0000
@@ -312,48 +312,53 @@
user = models.OneToOneField(User)
- def reset_authorisation_token(self):
- """Create (if necessary) and regenerate the keys for the Consumer and
- the related Token of the OAuth authorisation.
-
- :return: A tuple containing the Consumer and the Token that were reset.
+ def get_authorisation_tokens(self):
+ """Fetches all the user's OAuth tokens.
+
+ :return: A QuerySet of the tokens.
+ :rtype: django.db.models.query.QuerySet_
+
+ .. _django.db.models.query.QuerySet: https://docs.djangoproject.com/
+ en/dev/ref/models/querysets/
+
+ """
+ return Token.objects.select_related().filter(
+ user=self.user, token_type=Token.ACCESS,
+ is_approved=True).order_by('id')
+
+ def create_authorisation_token(self):
+ """Create a new Token and its related Consumer (OAuth authorisation).
+
+ :return: A tuple containing the Consumer and the Token that were
+ created.
:rtype: tuple
"""
- consumer, _ = Consumer.objects.get_or_create(
- user=self.user, name=GENERIC_CONSUMER,
- defaults={'status': 'accepted'})
+ consumer = Consumer.objects.create(
+ user=self.user, name=GENERIC_CONSUMER, status='accepted')
consumer.generate_random_codes()
# This is a 'generic' consumer aimed to service many clients, hence
# we don't authenticate the consumer with key/secret key.
consumer.secret = ''
consumer.save()
- token, _ = Token.objects.get_or_create(
+ token = Token.objects.create(
user=self.user, token_type=Token.ACCESS, consumer=consumer,
- defaults={'is_approved': True})
+ is_approved=True)
token.generate_random_codes()
return consumer, token
- def get_authorisation_consumer(self):
- """Returns the OAuth Consumer attached to the related User_.
-
- :return: The attached Consumer.
- :rtype: piston.models.Consumer
-
- """
- return Consumer.objects.get(user=self.user, name=GENERIC_CONSUMER)
-
- def get_authorisation_token(self):
- """Returns the OAuth authorisation token attached to the related User_.
-
- :return: The attached Token.
- :rtype: piston.models.Token
-
- """
- consumer = self.get_authorisation_consumer()
- token = Token.objects.get(
- user=self.user, token_type=Token.ACCESS, consumer=consumer)
- return token
+ def delete_authorisation_token(self, token_key):
+ """Delete the user's OAuth token wich key token_key.
+
+ :param token_key: The key of the token to be deleted.
+ :type token_key: str
+ :raises: django.http.Http404_
+
+ """
+ token = get_object_or_404(
+ Token, user=self.user, token_type=Token.ACCESS, key=token_key)
+ token.consumer.delete()
+ token.delete()
# When a user is created: create the related profile and the default
@@ -364,7 +369,7 @@
profile = UserProfile.objects.create(user=instance)
# Create initial authorisation token.
- profile.reset_authorisation_token()
+ profile.create_authorisation_token()
# Connect the 'create_user' method to the post save signal of User.
post_save.connect(create_user, sender=User)
=== added file 'src/maasserver/static/img/delete.png'
Binary files src/maasserver/static/img/delete.png 1970-01-01 00:00:00 +0000 and src/maasserver/static/img/delete.png 2012-02-14 10:48:33 +0000 differ
=== modified file 'src/maasserver/static/js/node_add.js'
--- src/maasserver/static/js/node_add.js 2012-02-10 09:12:00 +0000
+++ src/maasserver/static/js/node_add.js 2012-02-14 10:48:33 +0000
@@ -25,12 +25,12 @@
AddNodeWidget.ATTRS = {
- /**
- * The number MAC Addresses fields on the form.
- *
- * @attribute nb_mac_fields
- * @type integer
- */
+ /**
+ * The number MAC Addresses fields on the form.
+ *
+ * @attribute nb_mac_fields
+ * @type integer
+ */
nb_mac_fields: {
getter: function() {
return this.get(
=== modified file 'src/maasserver/static/js/prefs.js'
--- src/maasserver/static/js/prefs.js 2012-02-10 09:45:34 +0000
+++ src/maasserver/static/js/prefs.js 2012-02-14 10:48:33 +0000
@@ -20,6 +20,16 @@
TokenWidget.NAME = 'profile-widget';
+TokenWidget.ATTRS = {
+ // The number of tokens displayed.
+ nb_tokens: {
+ readOnly: true,
+ getter: function() {
+ return this.get('srcNode').all('.bundle').size();
+ }
+ }
+};
+
Y.extend(TokenWidget, Y.Widget, {
displayError: function(message) {
@@ -27,25 +37,65 @@
},
initializer: function(cfg) {
- this.regenerate_link = Y.Node.create('<a />')
+ this.create_link = Y.Node.create('<a />')
.set('href', '#')
- .set('id','regenerate_tokens')
- .set('text', "Regenerate the tokens");
- this.status_node = Y.Node.create('<span />')
- .set('id','regenerate_error');
+ .set('id','create_token')
+ .set('text', "Create a new API token");
+ this.status_node = Y.Node.create('<div />')
+ .set('id','create_error');
this.spinnerNode = Y.Node.create('<img />')
.set('src', MAAS_config.uris.statics + 'img/spinner.gif');
- this.get('srcNode').one('#tokens_regeneration_placeholder')
- .append(this.regenerate_link)
+ this.get('srcNode').one('#token_creation_placeholder')
+ .append(this.create_link)
.append(this.status_node);
},
+ bindDeleteRow: function(row) {
+ var self = this;
+ var delete_link = row.one('a.delete-link');
+ delete_link.on('click', function(e) {
+ e.preventDefault();
+ self.deleteToken(row);
+ });
+ },
+
bindUI: function() {
var self = this;
- this.regenerate_link.on('click', function(e) {
+ this.create_link.on('click', function(e) {
e.preventDefault();
- self.regenerateTokens();
- });
+ self.requestKeys();
+ });
+ Y.each(this.get('srcNode').all('.bundle'), function(row) {
+ self.bindDeleteRow(row);
+ });
+ },
+
+ /**
+ * Delete the token contained in the provided row.
+ * Call the API to delete the token and then remove the table row.
+ *
+ * @method deleteToken
+ */
+ deleteToken: function(row) {
+ var token_key = row.one('td').get('id');
+ var self = this;
+ var cfg = {
+ method: 'POST',
+ data: 'op=delete_authorisation_token&token_key=' + token_key,
+ sync: false,
+ on: {
+ start: Y.bind(self.showSpinner, self),
+ end: Y.bind(self.hideSpinner, self),
+ success: function(id, out) {
+ row.remove();
+ },
+ failure: function(id, out) {
+ self.displayError('Unable to delete the token.');
+ }
+ }
+ };
+ var request = module._io.io(
+ MAAS_config.uris.account_handler, cfg);
},
showSpinner: function() {
@@ -57,38 +107,75 @@
this.spinnerNode.remove();
},
- regenerateTokens: function() {
+ /**
+ * Create a single string token from a key set.
+ *
+ * A key set is composed of 3 keys: consumer_key, token_key, token_secret.
+ * For an easy copy and paste experience, the string handed over to the
+ * user is a colon separated concatenation of these keys called 'token'.
+ *
+ * @method createTokenFromKeys
+ */
+ createTokenFromKeys: function(consumer_key, token_key, token_secret) {
+ return consumer_key + ':' + token_key + ':' + token_secret;
+ },
+
+ /**
+ * Add a token to the list of tokens.
+ *
+ * @method addToken
+ */
+ addToken: function(token, token_key) {
+ var tbody = this.get('srcNode').one('tbody');
+ var row = Y.Node.create('<tr />')
+ .addClass('bundle')
+ .append(Y.Node.create('<td />')
+ .set('id', token_key)
+ .set('text', token))
+ .append(Y.Node.create('<td />')
+ .append(Y.Node.create('<a />')
+ .set('href', '#').addClass('delete-link')
+ .append(Y.Node.create('<img />')
+ .set('title', 'Delete token')
+ .set(
+ 'src',
+ MAAS_config.uris.statics + 'img/delete.png'))));
+ tbody.append(row);
+ this.bindDeleteRow(row);
+ },
+
+ /**
+ * Request a new OAuth key set from the API.
+ *
+ * @method requestKeys
+ */
+ requestKeys: function() {
var self = this;
var cfg = {
method: 'POST',
- data: 'op=reset_authorisation_token',
+ data: 'op=create_authorisation_token',
sync: false,
on: {
start: Y.bind(self.showSpinner, self),
end: Y.bind(self.hideSpinner, self),
success: function(id, out) {
- var token;
+ var keys;
try {
- token = JSON.parse(out.response);
+ keys = JSON.parse(out.response);
}
catch(e) {
// Parsing error.
- displayError('Unable to regenerate the tokens.');
+ self.displayError('Unable to create a new token.');
}
- // Update the 3 tokens (consumer key, token key and
- // token secret).
- self.get(
- 'srcNode').one(
- '#token_key').set('text', token.token_key);
- self.get(
- 'srcNode').one(
- '#token_secret').set('text', token.token_secret);
- self.get(
- 'srcNode').one(
- '#consumer_key').set('text', token.consumer_key);
+ // Generate a token from the keys.
+ var token = self.createTokenFromKeys(
+ keys.consumer_key, keys.token_key,
+ keys.token_secret);
+ // Add the new token to the list of tokens.
+ self.addToken(token, keys.token_key);
},
failure: function(id, out) {
- displayError('Unable to regenerate the tokens.');
+ self.displayError('Unable to create a new token.');
}
}
};
=== modified file 'src/maasserver/static/js/tests/test_prefs.html'
--- src/maasserver/static/js/tests/test_prefs.html 2012-02-09 17:11:07 +0000
+++ src/maasserver/static/js/tests/test_prefs.html 2012-02-14 10:48:33 +0000
@@ -26,14 +26,20 @@
<span id="suite">maas.prefs.tests</span>
<script type="text/x-template" id="api-template">
<div id="placeholder">
- <div id="consumer_key">
- </div>
- <div id="token_key">
- </div>
- <div id="token_secret">
- </div>
- <div id="tokens_regeneration_placeholder">
- </div>
+ <table>
+ <tbody>
+ <tr class="bundle">
+ <td id="tokenkey1">Sample token 1</td>
+ <td><a class="delete-link" href="#">Delete</a></td>
+ </tr>
+ <tr class="bundle">
+ <td id="tokenkey2">Sample token 2</td>
+ <td><a class="delete-link" href="#">Delete</a></td>
+ </tr>
+ </tbody>
+ </table>
+ <p id="token_creation_placeholder">
+ </p>
</div>
</script>
</body>
=== modified file 'src/maasserver/static/js/tests/test_prefs.js'
--- src/maasserver/static/js/tests/test_prefs.js 2012-02-09 14:29:09 +0000
+++ src/maasserver/static/js/tests/test_prefs.js 2012-02-14 10:48:33 +0000
@@ -23,49 +23,161 @@
var widget = new module.TokenWidget({srcNode: '#placeholder'});
this.addCleanup(function() { widget.destroy(); });
widget.render();
- var regenerate_link = widget.get('srcNode').one('#regenerate_tokens');
- Y.Assert.isNotNull(regenerate_link);
+ // The "create a new API token" has been created.
+ var create_link = widget.get('srcNode').one('#create_token');
+ Y.Assert.isNotNull(create_link);
Y.Assert.areEqual(
- "Regenerate the tokens", regenerate_link.get('text'));
- var status_node = widget.get('srcNode').one('#regenerate_error');
+ "Create a new API token", create_link.get('text'));
+ // The placeholder node for errors has been created.
+ var status_node = widget.get('srcNode').one('#create_error');
Y.Assert.isNotNull(status_node);
- },
-
- testRegenerateTokensCall: function() {
- var mockXhr = Y.Mock();
- Y.Mock.expect(mockXhr, {
- method: 'io',
- args: [MAAS_config.uris.account_handler, Y.Mock.Value.Any]
- });
- this.mockIO(mockXhr, module);
- var widget = new module.TokenWidget({srcNode: '#placeholder'});
- this.addCleanup(function() { widget.destroy(); });
- widget.render();
- var link = widget.get('srcNode').one('#regenerate_tokens');
- link.simulate('click');
- Y.Mock.verify(mockXhr);
- },
-
- testRegenerateTokensUpdatesTokens: function() {
- var mockXhr = new Y.Base();
- mockXhr.io = function(url, cfg) {
+ Y.Assert.areEqual(
+ '',
+ widget.get('srcNode').one('#create_error').get('text'));
+ },
+
+ test_nb_tokens: function() {
+ var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
+ widget.render();
+ Y.Assert.areEqual(2, widget.get('nb_tokens'));
+ },
+
+ testDeleteTokenCall: function() {
+ // A click on the delete link calls the API to delete a token.
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.io = function(url, cfg) {
+ fired = true;
+ Y.Assert.areEqual(MAAS_config.uris.account_handler, url);
+ Y.Assert.areEqual(
+ "op=delete_authorisation_token&token_key=tokenkey1",
+ cfg.data);
+ };
+ this.mockIO(mockXhr, module);
+ var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
+ widget.render();
+ var link = widget.get('srcNode').one('.delete-link');
+ link.simulate('click');
+ Y.Assert.isTrue(fired);
+ },
+
+ testDeleteTokenFail: function() {
+ // If the API call to delete a token fails, an error is displayed.
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.io = function(url, cfg) {
+ fired = true;
+ cfg.on.failure(3);
+ };
+ this.mockIO(mockXhr, module);
+ var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
+ widget.render();
+ var link = widget.get('srcNode').one('.delete-link');
+ link.simulate('click');
+ Y.Assert.isTrue(fired);
+ Y.Assert.areEqual(
+ 'Unable to delete the token.',
+ widget.get('srcNode').one('#create_error').get('text'));
+ },
+
+ testDeleteTokenDisplay: function() {
+ // When the token is successfully deleted by the API, the
+ // corresponding row is deleted.
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.io = function(url, cfg) {
+ fired = true;
+ cfg.on.success(3);
+ };
+ this.mockIO(mockXhr, module);
+ var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
+ widget.render();
+ var link = widget.get('srcNode').one('.delete-link');
+ Y.Assert.isNotNull(Y.one('#tokenkey1'));
+ link.simulate('click');
+ Y.Assert.isTrue(fired);
+ Y.Assert.isNull(Y.one('#tokenkey1'));
+ Y.Assert.isNotNull(Y.one('#tokenkey2'));
+ Y.Assert.areEqual(1, widget.get('nb_tokens'));
+ },
+
+ test_createTokenFromKeys: function() {
+ var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
+ var token = widget.createTokenFromKeys(
+ 'consumer_key', 'token_key', 'token_secret');
+ Y.Assert.areEqual('consumer_key:token_key:token_secret', token);
+ },
+
+ testCreateTokenCall: function() {
+ // A click on the "create a new token" link calls the API to
+ // create a token.
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.io = function(url, cfg) {
+ fired = true;
+ Y.Assert.areEqual(MAAS_config.uris.account_handler, url);
+ Y.Assert.areEqual(
+ "op=create_authorisation_token",
+ cfg.data);
+ };
+ this.mockIO(mockXhr, module);
+ var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
+ widget.render();
+ var create_link = widget.get('srcNode').one('#create_token');
+ create_link.simulate('click');
+ Y.Assert.isTrue(fired);
+ },
+
+ testCreateTokenFail: function() {
+ // If the API call to create a token fails, an error is displayed.
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.io = function(url, cfg) {
+ fired = true;
+ cfg.on.failure(3);
+ };
+ this.mockIO(mockXhr, module);
+ var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
+ widget.render();
+ var create_link = widget.get('srcNode').one('#create_token');
+ create_link.simulate('click');
+ Y.Assert.isTrue(fired);
+ Y.Assert.areEqual(
+ 'Unable to create a new token.',
+ widget.get('srcNode').one('#create_error').get('text'));
+ },
+
+ testCreateTokenDisplay: function() {
+ // When a new token is successfully created by the API, a new
+ // corresponding row is added.
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.io = function(url, cfg) {
+ fired = true;
var response = {
- token_key: 'token_key', token_secret: 'token_secret',
- consumer_key: 'consumer_key'};
+ consumer_key: 'consumer_key',
+ token_key: 'token_key',
+ token_secret: 'token_secret'
+ };
cfg.on.success(3, {response: Y.JSON.stringify(response)});
};
this.mockIO(mockXhr, module);
var widget = new module.TokenWidget({srcNode: '#placeholder'});
+ this.addCleanup(function() { widget.destroy(); });
widget.render();
- var link = widget.get('srcNode').one('#regenerate_tokens');
- link.simulate('click');
- var src_node = widget.get('srcNode');
- Y.Assert.areEqual('token_key', src_node.one('#token_key').get('text'));
- Y.Assert.areEqual(
- 'token_secret', src_node.one('#token_secret').get('text'));
- Y.Assert.areEqual(
- 'consumer_key', src_node.one('#consumer_key').get('text'));
- }
+ var create_link = widget.get('srcNode').one('#create_token');
+ create_link.simulate('click');
+ Y.Assert.isTrue(fired);
+ Y.Assert.areEqual(3, widget.get('nb_tokens'));
+ Y.Assert.isNotNull(Y.one('#token_key'));
+ }
}));
=== modified file 'src/maasserver/templates/maasserver/prefs.html'
--- src/maasserver/templates/maasserver/prefs.html 2012-02-10 09:34:47 +0000
+++ src/maasserver/templates/maasserver/prefs.html 2012-02-14 10:48:33 +0000
@@ -49,37 +49,36 @@
</div>
<div id="api">
<p>
- This is the required information you need to provide to a third
+ An API token is the information you need to provide to a third
party application for it to be able to access the MaaS server
<a href="{% url 'api-doc' %}">API</a> on your behalf.
</p>
<p>
- <label>API consumer key:
- <img src="{{ STATIC_URL }}img/maybe.png"
- title="Note that the API consumer secret is the empty string"
- />
- </label>
- <div id="consumer_key">
- {{ user.get_profile.get_authorisation_consumer.key }}
- </div>
- </p>
- <p>
- <label>API consumer secret:</label>
- <div>'' (the empty string)</div>
- </p>
- <p>
- <label>API token key:</label>
- <div id="token_key">
- {{ user.get_profile.get_authorisation_token.key }}
- </div>
- </p>
- <p>
- <label>API token secret:</label>
- <div id="token_secret">
- {{ user.get_profile.get_authorisation_token.secret }}
- </div>
- </p>
- <p id="tokens_regeneration_placeholder" />
+ You can create as much API tokens as you require.
+ </p>
+ <table>
+ <thead>
+ <tr>
+ <th>API tokens</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for token in user.get_profile.get_authorisation_tokens %}
+ <tr class="bundle">
+ <td id="{{ token.key }}">
+ {{ token.consumer.key }}:{{ token.key }}:{{ token.secret }}</td>
+ <td>
+ <a href="#" class="delete-link">
+ <img title="Delete token"
+ src="{{ STATIC_URL }}img/delete.png" />
+ </a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <p id="token_creation_placeholder" />
</div>
<div id="password">
<form action="." method="post">
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-02-14 08:20:40 +0000
+++ src/maasserver/tests/test_api.py 2012-02-14 10:48:33 +0000
@@ -54,10 +54,11 @@
class OAuthAuthenticatedClient(Client):
+
def __init__(self, user):
super(OAuthAuthenticatedClient, self).__init__()
- consumer = user.get_profile().get_authorisation_consumer()
- token = user.get_profile().get_authorisation_token()
+ token = user.get_profile().get_authorisation_tokens()[0]
+ consumer = token.consumer
self.consumer = OAuthConsumer(str(consumer.key), str(consumer.secret))
self.token = OAuthToken(str(token.key), str(token.secret))
@@ -545,6 +546,41 @@
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+class AccountAPITest(APITestCase):
+
+ def test_create_authorisation_token(self):
+ # The api operation create_authorisation_token returns a json dict
+ # with the consumer_key, the token_key and the token_secret in it.
+ response = self.client.post(
+ '/api/account/', {'op': 'create_authorisation_token'})
+ parsed_result = json.loads(response.content)
+
+ self.assertEqual(
+ ['consumer_key', 'token_key', 'token_secret'],
+ sorted(parsed_result.keys()))
+ self.assertIsInstance(parsed_result['consumer_key'], basestring)
+ self.assertIsInstance(parsed_result['token_key'], basestring)
+ self.assertIsInstance(parsed_result['token_secret'], basestring)
+
+ def test_delete_authorisation_token_not_found(self):
+ # If the provided token_key does not exist (for the currently
+ # logged-in user), the api returns a 'Not Found' (404) error.
+ response = self.client.post(
+ '/api/account/',
+ {'op': 'delete_authorisation_token', 'token_key': 'no-such-token'})
+
+ self.assertEqual(httplib.NOT_FOUND, response.status_code)
+
+ def test_delete_authorisation_token_bad_request_no_token(self):
+ # token_key is a mandatory parameter when calling
+ # delete_authorisation_token. It it is not present in the request's
+ # parameters, the api returns a 'Bad Request' (400) error.
+ response = self.client.post(
+ '/api/account/', {'op': 'delete_authorisation_token'})
+
+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+
+
class FileStorageAPITest(APITestCase):
def setUp(self):
=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py 2012-02-13 16:32:31 +0000
+++ src/maasserver/tests/test_models.py 2012-02-14 10:48:33 +0000
@@ -31,6 +31,7 @@
from piston.models import (
Consumer,
KEY_SIZE,
+ SECRET_SIZE,
Token,
)
@@ -194,6 +195,17 @@
class UserProfileTest(TestCase):
+ def assertTokenValid(self, token):
+ self.assertIsInstance(token.key, basestring)
+ self.assertEqual(KEY_SIZE, len(token.key))
+ self.assertIsInstance(token.secret, basestring)
+ self.assertEqual(SECRET_SIZE, len(token.secret))
+
+ def assertConsumerValid(self, consumer):
+ self.assertIsInstance(consumer.key, basestring)
+ self.assertEqual(KEY_SIZE, len(consumer.key))
+ self.assertEqual('', consumer.secret)
+
def test_profile_creation(self):
# A profile is created each time a user is created.
user = factory.make_user()
@@ -205,23 +217,37 @@
user = factory.make_user()
consumers = Consumer.objects.filter(user=user, name=GENERIC_CONSUMER)
self.assertEqual([user], [consumer.user for consumer in consumers])
- self.assertEqual(
- consumers[0], user.get_profile().get_authorisation_consumer())
- self.assertEqual(GENERIC_CONSUMER, consumers[0].name)
- self.assertEqual(KEY_SIZE, len(consumers[0].key))
- # The generic consumer has an empty secret.
- self.assertEqual(0, len(consumers[0].secret))
+ self.assertConsumerValid(consumers[0])
def test_token_creation(self):
# A token is created each time a user is created.
user = factory.make_user()
tokens = Token.objects.filter(user=user)
self.assertEqual([user], [token.user for token in tokens])
- self.assertEqual(
- tokens[0], user.get_profile().get_authorisation_token())
- self.assertIsInstance(tokens[0].key, unicode)
- self.assertEqual(KEY_SIZE, len(tokens[0].key))
- self.assertEqual(Token.ACCESS, tokens[0].token_type)
+ self.assertTokenValid(tokens[0])
+
+ def test_create_authorisation_token(self):
+ user = factory.make_user()
+ profile = user.get_profile()
+ consumer, token = profile.create_authorisation_token()
+ self.assertEqual(consumer, token.consumer)
+ self.assertEqual(user, token.user)
+ self.assertEqual(user, consumer.user)
+ self.assertConsumerValid(consumer)
+ self.assertTokenValid(token)
+
+ def test_get_authorisation_tokens(self):
+ user = factory.make_user()
+ other_user = factory.make_user()
+ profile = user.get_profile()
+ other_profile = other_user.get_profile()
+ _, token = profile.create_authorisation_token()
+ other_profile.create_authorisation_token()
+ tokens = profile.get_authorisation_tokens()
+ # This user has 2 tokens: the one that was created automatically
+ # when the user was created plus the one we've created manually.
+ self.assertEqual(2, tokens.count())
+ self.assertEqual(token, list(tokens.order_by('id'))[1])
class FileStorageTest(TestCase):
=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py 2012-02-10 12:59:12 +0000
+++ src/maasserver/tests/test_views.py 2012-02-14 10:48:33 +0000
@@ -41,23 +41,20 @@
def test_prefs_GET_api(self):
# The preferences page (api tab) displays the API access tokens.
user = self.logged_in_user
+ # Create a few tokens.
+ for i in xrange(3):
+ user.get_profile().create_authorisation_token()
response = self.client.get('/account/prefs/?tab=1')
doc = fromstring(response.content)
- # The consumer key and the token key/secret are displayed.
- consumer = user.get_profile().get_authorisation_consumer()
- token = user.get_profile().get_authorisation_token()
- self.assertSequenceEqual(
- [consumer.key],
- [elem.text.strip() for elem in
- doc.cssselect('div#consumer_key')])
- self.assertSequenceEqual(
- [token.key],
- [elem.text.strip() for elem in
- doc.cssselect('div#token_key')])
- self.assertSequenceEqual(
- [token.secret],
- [elem.text.strip() for elem in
- doc.cssselect('div#token_secret')])
+ # The OAuth tokens are displayed.
+ for token in user.get_profile().get_authorisation_tokens():
+ consumer = token.consumer
+ # The token string is a compact representation of the keys.
+ token_string = '%s:%s:%s' % (consumer.key, token.key, token.secret)
+ self.assertSequenceEqual(
+ [token_string],
+ [elem.text.strip() for elem in
+ doc.cssselect('td#%s' % token.key)])
def test_prefs_POST_profile(self):
# The preferences page allows the user the update its profile