Commit 833d5490 by Bach Dániel

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

Feature transfer template ownership

Closes #160

See merge request !264
parents af0882f5 66afc59c
......@@ -50,6 +50,21 @@ $(function () {
return false;
});
$('.tx-tpl-ownership').click(function(e) {
$.ajax({
type: 'GET',
url: $('.tx-tpl-ownership').attr('href'),
success: function(data) {
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
}
});
return false;
});
$('.template-choose').click(function(e) {
$.ajax({
type: 'GET',
......
......@@ -6,15 +6,15 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% trans "Ownership transfer" %}
{% trans "Ownership transfer" %}
</h3>
</div>
<div class="panel-body">
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
virtual machine <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the host's owner?
{% endblocktrans %}
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
virtual machine <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the host's owner?
{% endblocktrans %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% trans "Ownership transfer" %}
</h3>
</div>
<div class="panel-body">
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
template <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the template's owner?
{% endblocktrans %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default" href="{% url "dashboard.index" %}">{% trans "No" %}</a>
<input type="hidden" name="key" value="{{ key }}"/>
<button class="btn btn-danger" type="submit">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
......@@ -81,6 +81,26 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="fa fa-user"></i> {% trans "Owner" %}</h4>
</div>
<div class="panel-body">
{% if user == object.owner %}
{% blocktrans %}You are the current owner of this template.{% endblocktrans %}
{% else %}
{% url "dashboard.views.profile" username=object.owner.username as url %}
{% blocktrans with owner=object.owner name=object.owner.get_full_name%}
The current owner of this template is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %}
{% endif %}
{% if user == object.owner or user.is_superuser %}
<a href="{% url "dashboard.views.template-transfer-ownership" object.pk %}"
class="btn btn-link tx-tpl-ownership">{% trans "Transfer ownership..." %}</a>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4>
</div>
<div class="panel-body">
......
{% load i18n %}
<div class="pull-right">
<form action="{% url "dashboard.views.template-transfer-ownership" pk=object.pk %}" method="POST" style="max-width: 400px;">
{% csrf_token %}
<label>
{{ form.name.label }}
</label>
<div class="input-group">
{{form.name}}
<div class="input-group-btn">
<input type="submit" value="{% trans "Save" %}" class="btn btn-primary">
</div>
</div>
</form>
</div>
......@@ -4,8 +4,9 @@
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
{% else %}
{% blocktrans with owner=instance.owner %}
The current owner of this instance is {{owner}}.
{% url "dashboard.views.profile" username=instance.owner.username as url %}
{% blocktrans with owner=instance.owner name=instance.owner.get_full_name%}
The current owner of this instance is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
......
......@@ -27,8 +27,8 @@ from .views import (
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView,
......@@ -48,6 +48,8 @@ from .views import (
toggle_template_tutorial,
ClientCheck, TokenLogin,
VmGraphView, NodeGraphView, NodeListGraphView,
TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView,
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
......@@ -78,6 +80,8 @@ urlpatterns = patterns(
name="dashboard.views.template-list"),
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"),
url(r'^template/(?P<pk>\d+)/tx/$', TransferTemplateOwnershipView.as_view(),
name='dashboard.views.template-transfer-ownership'),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
......@@ -86,7 +90,7 @@ urlpatterns = patterns(
name='dashboard.views.detail-vnc'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferInstanceOwnershipView.as_view(),
name='dashboard.views.vm-transfer-ownership'),
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
......@@ -108,8 +112,12 @@ urlpatterns = patterns(
name='dashboard.views.node-detail'),
url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(),
name='dashboard.views.node-addtrait'),
url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(),
url(r'^vm/tx/(?P<key>.*)/?$',
TransferInstanceOwnershipConfirmView.as_view(),
name='dashboard.views.vm-transfer-ownership-confirm'),
url(r'^template/tx/(?P<key>.*)/?$',
TransferTemplateOwnershipConfirmView.as_view(),
name='dashboard.views.template-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(),
......
......@@ -26,7 +26,7 @@ from django.core.urlresolvers import reverse, reverse_lazy
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import (
TemplateView, CreateView, DeleteView, UpdateView,
)
......@@ -44,7 +44,10 @@ from ..forms import (
)
from ..tables import TemplateListTable, LeaseListTable
from .util import AclUpdateView, FilterMixin
from .util import (
AclUpdateView, FilterMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
)
logger = logging.getLogger(__name__)
......@@ -488,3 +491,20 @@ class LeaseDelete(LoginRequiredMixin, DeleteView):
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-template-ownership.html"
model = InstanceTemplate
class TransferTemplateOwnershipView(TransferOwnershipView):
confirm_view = TransferTemplateOwnershipConfirmView
model = InstanceTemplate
notification_msg = ugettext_noop(
'%(user)s offered you to take the ownership of '
'his/her template called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>')
token_url = 'dashboard.views.template-transfer-ownership-confirm'
template = "dashboard/template-tx-owner.html"
......@@ -24,14 +24,15 @@ from urlparse import urljoin
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied
from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View
from django.views.generic.detail import SingleObjectMixin
......@@ -40,7 +41,8 @@ from braces.views._access import AccessMixin
from celery.exceptions import TimeoutError
from common.models import HumanReadableException, HumanReadableObject
from ..models import GroupProfile
from ..models import GroupProfile, Profile
from ..forms import TransferOwnershipForm
logger = logging.getLogger(__name__)
saml_available = hasattr(settings, "SAML_CONFIG")
......@@ -563,3 +565,132 @@ class GraphMixin(object):
def absolute_url(url):
return urljoin(settings.DJANGO_URL, url)
class TransferOwnershipView(CheckedDetailView, DetailView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(TransferOwnershipView, self).get_context_data(
*args, **kwargs)
context['form'] = TransferOwnershipForm()
context.update({
'box_title': _("Transfer ownership"),
'ajax_title': True,
'template': self.template,
})
return context
def post(self, request, *args, **kwargs):
form = TransferOwnershipForm(request.POST)
if not form.is_valid():
return self.get(request)
try:
new_owner = search_user(request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError:
raise SuspiciousOperation()
obj = self.get_object()
if not (obj.owner == request.user or
request.user.is_superuser):
raise PermissionDenied()
token = signing.dumps(
(obj.pk, new_owner.pk),
salt=self.confirm_view.get_salt())
token_path = reverse(self.token_url, args=[token])
try:
new_owner.profile.notify(
ugettext_noop('Ownership offer'),
self.notification_msg,
{'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(obj.get_absolute_url())
class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred to you.")
@classmethod
def get_salt(cls):
return unicode(cls) + unicode(cls.model)
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user)
except PermissionDenied:
messages.error(request, _('This token is for an other user.'))
raise
except SuspiciousOperation:
messages.error(request, _('This token is invalid or has expired.'))
raise PermissionDenied()
return render(request, self.template,
dictionary={'instance': instance, 'key': key})
def change_owner(self, instance, new_owner):
instance.owner = new_owner
instance.clean()
instance.save()
def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token.
"""
instance, owner = self.get_instance(key, request.user)
old = instance.owner
self.change_owner(instance, request.user)
messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'),
{'instance': instance})
return redirect(instance.get_absolute_url())
def get_instance(self, key, user):
"""Get object based on signed token.
"""
try:
instance, new_owner = (
signing.loads(key, max_age=self.max_age,
salt=self.get_salt()))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
try:
instance = self.model.objects.get(id=instance)
except self.model.DoesNotExist as e:
logger.error('Tried token to nonexistent instance %d. '
'Token: %s, user: %s. %s',
instance, key, unicode(user), unicode(e))
raise Http404()
if new_owner != user.pk:
logger.error('%s (%d) tried the token for %s. Token: %s.',
unicode(user), user.pk, new_owner, key)
raise PermissionDenied()
return (instance, new_owner)
......@@ -29,7 +29,7 @@ from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404, render
from django.shortcuts import redirect, get_object_or_404
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import (
......@@ -37,7 +37,7 @@ from django.utils.translation import (
)
from django.views.decorators.http import require_GET
from django.views.generic import (
UpdateView, ListView, TemplateView, DeleteView, DetailView, View,
UpdateView, ListView, TemplateView, DeleteView
)
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
......@@ -54,16 +54,17 @@ from vm.models import (
)
from .util import (
CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView,
FormOperationMixin, FilterMixin, search_user, GraphMixin,
FormOperationMixin, FilterMixin, GraphMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
)
from ..forms import (
AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm,
VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
TransferOwnershipForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmMigrateForm, VmDeployForm,
)
from ..models import Favourite, Profile
from ..models import Favourite
logger = logging.getLogger(__name__)
......@@ -1306,139 +1307,29 @@ class FavouriteView(TemplateView):
return HttpResponse("Added.")
class TransferOwnershipView(CheckedDetailView, DetailView):
class TransferInstanceOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-instance-ownership.html"
model = Instance
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(TransferOwnershipView, self).get_context_data(
*args, **kwargs)
context['form'] = TransferOwnershipForm()
context.update({
'box_title': _("Transfer ownership"),
'ajax_title': True,
'template': "dashboard/vm-detail/tx-owner.html",
})
return context
def post(self, request, *args, **kwargs):
form = TransferOwnershipForm(request.POST)
if not form.is_valid():
return self.get(request)
try:
new_owner = search_user(request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError:
raise SuspiciousOperation()
obj = self.get_object()
if not (obj.owner == request.user or
request.user.is_superuser):
raise PermissionDenied()
token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt())
token_path = reverse(
'dashboard.views.vm-transfer-ownership-confirm', args=[token])
try:
new_owner.profile.notify(
ugettext_noop('Ownership offer'),
ugettext_noop('%(user)s offered you to take the ownership of '
'his/her virtual machine called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>'),
{'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}))
def change_owner(self, instance, new_owner):
with instance.activity(
code_suffix='ownership-transferred',
readable_name=ugettext_noop("transfer ownership"),
concurrency_check=False, user=new_owner):
super(TransferInstanceOwnershipConfirmView, self).change_owner(
instance, new_owner)
class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred to you.")
@classmethod
def get_salt(cls):
return unicode(cls)
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user)
except PermissionDenied:
messages.error(request, _('This token is for an other user.'))
raise
except SuspiciousOperation:
messages.error(request, _('This token is invalid or has expired.'))
raise PermissionDenied()
return render(request,
"dashboard/confirm/base-transfer-ownership.html",
dictionary={'instance': instance, 'key': key})
def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token.
"""
instance, owner = self.get_instance(key, request.user)
old = instance.owner
with instance.activity(code_suffix='ownership-transferred',
concurrency_check=False, user=request.user):
instance.owner = request.user
instance.clean()
instance.save()
messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'),
{'instance': instance})
return redirect(instance.get_absolute_url())
def get_instance(self, key, user):
"""Get object based on signed token.
"""
try:
instance, new_owner = (
signing.loads(key, max_age=self.max_age,
salt=self.get_salt()))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
try:
instance = Instance.objects.get(id=instance)
except Instance.DoesNotExist as e:
logger.error('Tried token to nonexistent instance %d. '
'Token: %s, user: %s. %s',
instance, key, unicode(user), unicode(e))
raise Http404()
if new_owner != user.pk:
logger.error('%s (%d) tried the token for %s. Token: %s.',
unicode(user), user.pk, new_owner, key)
raise PermissionDenied()
return (instance, new_owner)
class TransferInstanceOwnershipView(TransferOwnershipView):
confirm_view = TransferInstanceOwnershipConfirmView
model = Instance
notification_msg = ugettext_noop(
'%(user)s offered you to take the ownership of '
'his/her virtual machine called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>')
token_url = 'dashboard.views.vm-transfer-ownership-confirm'
template = "dashboard/vm-detail/tx-owner.html"
@login_required
......
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