Commit 031f3fcc by Őry Máté

Merge branch 'feature-tx-ownership' into 'master'

Feature: Transer Ownership

Fixes #39
parents 9f818d85 e62e9383
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
<div class="notification-message-text"> <div class="notification-message-text">
{{ n.message }} {{ n.message|safe }}
</div> </div>
</li> </li>
{% empty %} {% empty %}
......
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
Your ownership offer of {{instance}} has been accepted by {{user}}.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
{{user}} offered you to take the ownership of his/her virtual machine
called {{instance}}.{%endblocktrans%}
<a href="{{token}}" class="btn btn-success btn-small">{%trans "Accept"%}</a>
{% load i18n %} {% load i18n %}
<h3>{% trans "Owner" %}</h3> <h3>{% trans "Owner" %}</h3>
<p> <p>
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %} {% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
<a href="#" class="btn btn-link">{% trans "Transfer ownership..." %}</a> {% else %}
{% blocktrans with owner=instance.owner %}
The current owner of this instance is {{owner}}.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
<a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}"
class="btn btn-link">{% trans "Transfer ownership..." %}</a>
{% endif %}
</p> </p>
<h3>{% trans "Permissions"|capfirst %}</h3> <h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %} <form action="{{acl.url}}" method="post">{% csrf_token %}
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="" method="POST">
{% csrf_token %} {% csrf_token %}
E-mail address or identifier of user:
<input name="name"> <input name="name">
<input type="submit"> <input type="submit">
</form> </form>
......
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation
from vm.models import Instance, InstanceTemplate, Lease, Node from vm.models import Instance, InstanceTemplate, Lease, Node
from ..models import Profile
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan from firewall.models import Vlan
class VmDetailTest(TestCase): class LoginMixin(object):
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
class VmDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json'] fixtures = ['test-vm-fixture.json']
def setUp(self): def setUp(self):
...@@ -32,11 +41,6 @@ class VmDetailTest(TestCase): ...@@ -32,11 +41,6 @@ class VmDetailTest(TestCase):
self.us.delete() self.us.delete()
self.g1.delete() self.g1.delete()
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
def test_404_vm_page(self): def test_404_vm_page(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
...@@ -236,7 +240,7 @@ class VmDetailTest(TestCase): ...@@ -236,7 +240,7 @@ class VmDetailTest(TestCase):
assert self.u1.notification_set.get().status == 'read' assert self.u1.notification_set.get().status == 'read'
class VmDetailVncTest(TestCase): class VmDetailVncTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json'] fixtures = ['test-vm-fixture.json', 'node.json']
def setUp(self): def setUp(self):
...@@ -244,11 +248,6 @@ class VmDetailVncTest(TestCase): ...@@ -244,11 +248,6 @@ class VmDetailVncTest(TestCase):
self.u1.set_password('password') self.u1.set_password('password')
self.u1.save() self.u1.save()
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
def test_permitted_vm_console(self): def test_permitted_vm_console(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
...@@ -268,3 +267,70 @@ class VmDetailVncTest(TestCase): ...@@ -268,3 +267,70 @@ class VmDetailVncTest(TestCase):
inst.set_level(self.u1, 'user') inst.set_level(self.u1, 'user')
response = c.get('/dashboard/vm/1/vnctoken/') response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
class TransferOwnershipViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
Profile.objects.create(user=self.u1)
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
Profile.objects.create(user=self.u2)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
Profile.objects.create(user=self.us)
inst = Instance.objects.get(pk=1)
inst.owner = self.u1
inst.save()
def test_non_owner_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user2')
with self.assertRaises(SuspiciousOperation):
c.post('/dashboard/vm/1/tx/')
self.assertEqual(self.u2.notification_set.count(), c2)
def test_owned_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user1')
response = c.get('/dashboard/vm/1/tx/')
assert response.status_code == 200
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
self.assertEqual(self.u2.notification_set.count(), c2 + 1)
def test_transfer(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
def test_transfer_token_used_by_others(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
response = c.post(url) # token is for user2
assert response.status_code == 403
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u1.pk)
def test_transfer_by_superuser(self):
c = Client()
self.login(c, 'superuser')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
...@@ -57,7 +57,7 @@ urlpatterns = patterns( ...@@ -57,7 +57,7 @@ urlpatterns = patterns(
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
name='dashboard.views.node-detail'), name='dashboard.views.node-detail'),
url(r'^tx/$', TransferOwnershipConfirmView.as_view(), url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(),
name='dashboard.views.vm-transfer-ownership-confirm'), name='dashboard.views.vm-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(), url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
......
...@@ -37,7 +37,7 @@ from vm.models import (Instance, InstanceTemplate, InterfaceTemplate, ...@@ -37,7 +37,7 @@ from vm.models import (Instance, InstanceTemplate, InterfaceTemplate,
InstanceActivity, Node, instance_activity, Lease, InstanceActivity, Node, instance_activity, Lease,
Interface, NodeActivity) Interface, NodeActivity)
from firewall.models import Vlan, Host, Rule from firewall.models import Vlan, Host, Rule
from dashboard.models import Favourite from dashboard.models import Favourite, Profile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -1490,7 +1490,12 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1490,7 +1490,12 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
try: try:
new_owner = User.objects.get(username=request.POST['name']) new_owner = User.objects.get(username=request.POST['name'])
except User.DoesNotExist: except User.DoesNotExist:
raise Http404() new_owner = User.objects.get(email=request.POST['name'])
except User.DoesNotExist:
new_owner = User.objects.get(profile__org_id=request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError: except KeyError:
raise SuspiciousOperation() raise SuspiciousOperation()
...@@ -1501,29 +1506,41 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1501,29 +1506,41 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
token = signing.dumps((obj.pk, new_owner.pk), token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt()) salt=TransferOwnershipConfirmView.get_salt())
return HttpResponse("%s?key=%s" % ( token_path = reverse(
reverse('dashboard.views.vm-transfer-ownership-confirm'), token), 'dashboard.views.vm-transfer-ownership-confirm', args=[token])
content_type="text/plain") try:
new_owner.profile.notify(
_('Ownership offer'),
'dashboard/notifications/ownership-offer.html',
{'instance': obj, 'token': token_path})
except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.'))
else:
messages.success(request,
_('User %s is notified about the offer.') % (
unicode(new_owner), ))
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': obj.pk}))
class TransferOwnershipConfirmView(LoginRequiredMixin, View): class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600 max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred.") success_message = _("Ownership successfully transferred to you.")
@classmethod @classmethod
def get_salt(cls): def get_salt(cls):
return unicode(cls) return unicode(cls)
def get(self, request, *args, **kwargs): def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token. """Confirm ownership transfer based on token.
""" """
try:
key = request.GET['key']
logger.debug('Confirm dialog for token %s.', key) logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user) instance, new_owner = self.get_instance(key, request.user)
except KeyError: except PermissionDenied:
raise Http404()
except PermissionDenied():
messages.error(request, _('This token is for an other user.')) messages.error(request, _('This token is for an other user.'))
raise raise
except SuspiciousOperation: except SuspiciousOperation:
...@@ -1533,16 +1550,10 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1533,16 +1550,10 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"dashboard/confirm/base-transfer-ownership.html", "dashboard/confirm/base-transfer-ownership.html",
dictionary={'instance': instance, 'key': key}) dictionary={'instance': instance, 'key': key})
def post(self, request, *args, **kwargs): def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token. """Really transfer ownership based on token.
""" """
try:
key = request.POST['key']
instance, owner = self.get_instance(key, request.user) instance, owner = self.get_instance(key, request.user)
except KeyError:
logger.debug('Posted to %s without key field.',
unicode(self.__class__))
raise SuspiciousOperation()
old = instance.owner old = instance.owner
with instance_activity(code_suffix='ownership-transferred', with instance_activity(code_suffix='ownership-transferred',
...@@ -1553,6 +1564,11 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1553,6 +1564,11 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
messages.success(request, self.success_message) messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.', logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user)) unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
_('Ownership accepted'),
'dashboard/notifications/ownership-accepted.html',
{'instance': instance})
return HttpResponseRedirect(instance.get_absolute_url()) return HttpResponseRedirect(instance.get_absolute_url())
def get_instance(self, key, user): def get_instance(self, key, user):
...@@ -1562,15 +1578,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1562,15 +1578,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
instance, new_owner = ( instance, new_owner = (
signing.loads(key, max_age=self.max_age, signing.loads(key, max_age=self.max_age,
salt=self.get_salt())) salt=self.get_salt()))
except signing.BadSignature as e: except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
except ValueError as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
except TypeError as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s', logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e)) key, unicode(user), unicode(e))
raise SuspiciousOperation() raise SuspiciousOperation()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment