Commit 79a46294 by Bach Dániel

Merge remote-tracking branch 'origin/master' into feature-fix-acls

Conflicts:
	circle/dashboard/views.py
parents 0c7119c8 cce7935c
......@@ -126,6 +126,18 @@ class Profile(Model):
return self.get_display_name()
class FutureMember(Model):
org_id = CharField(max_length=64, help_text=_(
'Unique identifier of the person, e.g. a student number.'))
group = ForeignKey(Group)
class Meta:
unique_together = ('org_id', 'group')
def __unicode__(self):
return u"%s (%s)" % (self.org_id, self.group)
class GroupProfile(AclBase):
ACL_LEVELS = (
('operator', _('operator')),
......@@ -210,6 +222,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g))
g.user_set.add(sender)
for i in FutureMember.objects.filter(org_id=value):
i.group.user_set.add(sender)
i.delete()
owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]):
......
{% extends "base.html" %}
{% load i18n %}
{% block title-site %}Dashboard | CIRCLE{% endblock %}
{% block content %}
{% blocktrans with group=object member=member %}
Do you really want to remove {{member}} from {{group}}?
{% endblocktrans %}
<form action="" method="POST">{% csrf_token %}
<input type="submit" value="{% trans "Remove" %}" />
</form>
{% endblock %}
......@@ -71,16 +71,30 @@
</td>
</tr>
{% endfor %}
{% for i in future_users %}
<tr>
<td>
<i class="icon-user text-muted"></i>
</td>
<td> {{ i.org_id }} </td>
<td>
<a href="{% url "dashboard.views.remove-future-user" member_org_id=i.org_id group_pk=group.pk %}"
class="real-link btn-link btn-xs">
<i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a>
</td>
</tr>
{% endfor %}
<tr>
<td><i class="icon-plus"></i></td>
<td colspan="2">
<input type="text" class="form-control" name="list-new-name"placeholder="{% trans "Name of user" %}">
<input type="text" class="form-control" name="list-new-name"
placeholder="{% trans "Name of user" %}">
</td>
</tr>
</tbody>
</table>
<textarea name="list-new-namelist" class="form-control"
placeholder="{% trans "List of usernames (one per line)." %}"></textarea>
placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
......
......@@ -80,9 +80,9 @@
{% endif %}
</dd>
{% if instance.ipv6 %}
{% if instance.ipv6 and instance.get_connect_port %}
<dt>{% trans "Host (IPv6)" %}</dt>
<dd>{{ ipv6_host }}:<strong>{{ instance.ipv6_port }}</strong></dd>
<dd>{{ ipv6_host }}:<strong>{{ ipv6_port }}</strong></dd>
{% endif %}
<dt>{% trans "Username" %}</dt>
......
......@@ -2,10 +2,20 @@
{% for op in ops %}
{% if op.show_in_toolbar %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn btn-default btn-xs"
title="{{op.name}}: {{op.description}}">
{% if op.disabled %}
<span class="operation operation-{{op.op}} btn btn-{{op.effect}} disabled btn-xs">
{% else %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn
btn-{{op.effect}} btn-xs" title="{{op.name}}: {{op.description}}">
{% endif %}
<i class="icon-{{op.icon}}"></i>
<span class="sr-only">{{op.name}}</span>
<span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span>
{% if op.disabled %}
</span>
{% else %}
</a>
{% endif %}
{% endif %}
{% endfor %}
......@@ -92,7 +92,7 @@
{% if l.ipv4 %}
<tr>
<td>
{% display_portforward l %}
{% display_portforward4 l %}
</td>
<td><i class="icon-long-arrow-right"></i></td>
<td>
......@@ -124,7 +124,7 @@
{% if l.ipv6 %}
<tr>
<td>
{% display_portforward l %}
{% display_portforward6 l %}
</td>
<td><i class="icon-long-arrow-right"></i></td>
<td>
......
......@@ -6,10 +6,18 @@ register = template.Library()
LINKABLE_PORTS = {80: "http", 8080: "http", 443: "https", 21: "ftp"}
@register.simple_tag(name="display_portforward")
def display_pf(ports):
is_ipv6 = "ipv6" in ports
data = ports["ipv6" if is_ipv6 else "ipv4"]
@register.simple_tag(name="display_portforward4")
def display_pf4(ports):
return display_pf(ports, 'ipv4')
@register.simple_tag(name="display_portforward6")
def display_pf6(ports):
return display_pf(ports, 'ipv6')
def display_pf(ports, proto):
data = ports[proto]
if ports['private'] in LINKABLE_PORTS.keys():
href = "%s:%d" % (data['host'], data['port'])
......
......@@ -31,6 +31,7 @@ from .views import (
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate,
TemplateChoose,
UserCreationView,
......@@ -157,6 +158,9 @@ urlpatterns = patterns(
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(),
name="dashboard.views.remove-user"),
url(r'^group/(?P<group_pk>\d+)/remove/futureuser/(?P<member_org_id>.+)/$',
GroupRemoveFutureUserView.as_view(),
name="dashboard.views.remove-future-user"),
url(r'^group/create/$', GroupCreate.as_view(),
name='dashboard.views.group-create'),
url(r'^group/(?P<group_pk>\d+)/create/$',
......
......@@ -17,6 +17,7 @@
from __future__ import unicode_literals, absolute_import
from collections import OrderedDict
from itertools import chain
from os import getenv
import json
......@@ -74,7 +75,7 @@ from vm.models import (
)
from storage.models import Disk
from firewall.models import Vlan, Host, Rule
from .models import Favourite, Profile, GroupProfile
from .models import Favourite, Profile, GroupProfile, FutureMember
logger = logging.getLogger(__name__)
saml_available = hasattr(settings, "SAML_CONFIG")
......@@ -501,6 +502,7 @@ class OperationView(DetailView):
template_name = 'dashboard/operate.html'
show_in_toolbar = True
effect = None
@property
def name(self):
......@@ -510,6 +512,9 @@ class OperationView(DetailView):
def description(self):
return self.get_op().description
def is_preferred(self):
return self.get_op().is_preferred()
@classmethod
def get_urlname(cls):
return 'dashboard.vm.op.%s' % cls.op
......@@ -559,14 +564,16 @@ class OperationView(DetailView):
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod
def factory(cls, op, icon='cog'):
def factory(cls, op, icon='cog', effect='info'):
return type(str(cls.__name__ + op),
(cls, ), {'op': op, 'icon': icon})
(cls, ), {'op': op, 'icon': icon, 'effect': effect})
@classmethod
def bind_to_object(cls, instance):
def bind_to_object(cls, instance, **kwargs):
v = cls()
v.get_object = lambda: instance
for key, value in kwargs.iteritems():
setattr(v, key, value)
return v
......@@ -643,6 +650,7 @@ class VmMigrateView(VmOperationView):
op = 'migrate'
icon = 'truck'
effect = 'info'
template_name = 'dashboard/_vm-migrate.html'
def get_context_data(self, **kwargs):
......@@ -665,6 +673,7 @@ class VmSaveView(FormOperationMixin, VmOperationView):
op = 'save_as_template'
icon = 'save'
effect = 'info'
form_class = VmSaveForm
......@@ -690,21 +699,28 @@ class VmResourcesChangeView(VmOperationView):
*args, **kwargs)
vm_ops = {
'reset': VmOperationView.factory(op='reset', icon='bolt'),
'deploy': VmOperationView.factory(op='deploy', icon='play'),
'migrate': VmMigrateView,
'reboot': VmOperationView.factory(op='reboot', icon='refresh'),
'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
'shutdown': VmOperationView.factory(op='shutdown', icon='off'),
'save_as_template': VmSaveView,
'destroy': VmOperationView.factory(op='destroy', icon='remove'),
'sleep': VmOperationView.factory(op='sleep', icon='moon'),
'wake_up': VmOperationView.factory(op='wake_up', icon='sun'),
'create_disk': VmCreateDiskView,
'download_disk': VmDownloadDiskView,
'resources_change': VmResourcesChangeView,
}
vm_ops = OrderedDict([
('deploy', VmOperationView.factory(
op='deploy', icon='play', effect='success')),
('wake_up', VmOperationView.factory(
op='wake_up', icon='sun', effect='success')),
('sleep', VmOperationView.factory(
op='sleep', icon='moon', effect='info')),
('migrate', VmMigrateView),
('save_as_template', VmSaveView),
('reboot', VmOperationView.factory(
op='reboot', icon='refresh', effect='warning')),
('reset', VmOperationView.factory(
op='reset', icon='bolt', effect='warning')),
('shutdown', VmOperationView.factory(
op='shutdown', icon='off', effect='warning')),
('shut_off', VmOperationView.factory(
op='shut_off', icon='ban-circle', effect='warning')),
('destroy', VmOperationView.factory(
op='destroy', icon='remove', effect='danger')),
('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView),
])
def get_operations(instance, user):
......@@ -714,9 +730,11 @@ def get_operations(instance, user):
op = v.get_op_by_object(instance)
op.check_auth(user)
op.check_precond()
except Exception as e:
except PermissionDenied as e:
logger.debug('Not showing operation %s for %s: %s',
k, instance, unicode(e))
except Exception:
ops.append(v.bind_to_object(instance, disabled=True))
else:
ops.append(v.bind_to_object(instance))
return ops
......@@ -803,6 +821,8 @@ class GroupDetailView(CheckedDetailView):
context = super(GroupDetailView, self).get_context_data(**kwargs)
context['group'] = self.object
context['users'] = self.object.user_set.all()
context['future_users'] = FutureMember.objects.filter(
group=self.object)
context['acl'] = get_group_acl_data(self.object)
context['group_profile_form'] = GroupProfileUpdate.get_form_object(
self.request, self.object.profile)
......@@ -836,7 +856,11 @@ class GroupDetailView(CheckedDetailView):
entity = User.objects.get(username=name)
self.object.user_set.add(entity)
except User.DoesNotExist:
warning(request, _('User "%s" not found.') % name)
if saml_available:
FutureMember.objects.get_or_create(org_id=name,
group=self.object)
else:
warning(request, _('User "%s" not found.') % name)
def __add_list(self, request):
if not self.get_has_level()(request.user, 'operator'):
......@@ -1350,6 +1374,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
slug_field = 'pk'
slug_url_kwarg = 'group_pk'
read_level = 'operator'
member_key = 'member_pk'
def get_has_level(self):
return self.object.profile.has_level
......@@ -1391,7 +1416,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
object = self.get_object()
if not object.profile.has_level(request.user, 'operator'):
raise PermissionDenied()
self.remove_member(kwargs["member_pk"])
self.remove_member(kwargs[self.member_key])
success_url = self.get_success_url()
success_message = self.get_success_message()
if request.is_ajax():
......@@ -1404,6 +1429,31 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
return HttpResponseRedirect(success_url)
class GroupRemoveFutureUserView(GroupRemoveUserView):
member_key = 'member_org_id'
def get(self, request, member_org_id, *args, **kwargs):
self.member_org_id = member_org_id
return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
try:
context['member'] = FutureMember.objects.get(
org_id=self.member_org_id, group=self.get_object())
except FutureMember.DoesNotExist:
raise Http404()
return context
def remove_member(self, org_id):
FutureMember.objects.filter(org_id=org_id,
group=self.get_object()).delete()
def get_success_message(self):
return _("Future user successfully removed from group.")
class GroupRemoveAclUserView(GroupRemoveUserView):
def remove_member(self, pk):
......
......@@ -28,6 +28,7 @@ import re
alfanum_re = re.compile(r'^[A-Za-z0-9_-]+$')
domain_re = re.compile(r'^([A-Za-z0-9_-]\.?)+$')
domain_wildcard_re = re.compile(r'^(\*\.)?([A-Za-z0-9_-]\.?)+$')
ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$')
reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
......@@ -216,12 +217,23 @@ def is_valid_domain(value):
return domain_re.match(value) is not None
def is_valid_domain_wildcard(value):
"""Check whether the parameter is a valid domain name."""
return domain_wildcard_re.match(value) is not None
def val_domain(value):
"""Validate whether the parameter is a valid domin name."""
if not is_valid_domain(value):
raise ValidationError(_(u'%s - invalid domain name') % value)
def val_domain_wildcard(value):
"""Validate whether the parameter is a valid domin name."""
if not is_valid_domain_wildcard(value):
raise ValidationError(_(u'%s - invalid domain name') % value)
def is_valid_reverse_domain(value):
"""Check whether the parameter is a valid reverse domain name."""
return reverse_domain_re.match(value) is not None
......
......@@ -27,6 +27,7 @@ from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain,
val_ipv6_template, val_domain, val_ipv4,
val_domain_wildcard,
val_ipv6, val_mx, convert_ipv4_to_ipv6,
IPNetworkField, IPAddressField)
from django.core.validators import MinValueValidator, MaxValueValidator
......@@ -695,8 +696,7 @@ class Host(models.Model):
:param private: Port number of host in subject.
"""
self.rules.filter(owner=self.owner, proto=proto, host=self,
dport=private).delete()
self.rules.filter(proto=proto, dport=private).delete()
def get_hostname(self, proto, public=True):
"""
......@@ -728,7 +728,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set.
"""
retval = []
for rule in self.rules.filter(owner=self.owner):
for rule in self.rules.all():
forward = {
'proto': rule.proto,
'private': rule.dport,
......@@ -770,9 +770,7 @@ class Host(models.Model):
if public_port else
None)
# IPv6
blocked = self.incoming_rules.exclude(
action='accept').filter(dport=port, proto=protocol).exists()
endpoints['ipv6'] = (self.ipv6, port) if not blocked else None
endpoints['ipv6'] = (self.ipv6, port) if public_port else None
return endpoints
@models.permalink
......@@ -821,7 +819,7 @@ class Domain(models.Model):
class Record(models.Model):
CHOICES_type = (('A', 'A'), ('CNAME', 'CNAME'), ('AAAA', 'AAAA'),
('MX', 'MX'), ('NS', 'NS'), ('PTR', 'PTR'), ('TXT', 'TXT'))
name = models.CharField(max_length=40, validators=[val_domain],
name = models.CharField(max_length=40, validators=[val_domain_wildcard],
blank=True, null=True, verbose_name=_('name'))
domain = models.ForeignKey('Domain', verbose_name=_('domain'))
host = models.ForeignKey('Host', blank=True, null=True,
......
......@@ -579,11 +579,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def get_connect_host(self, use_ipv6=False):
"""Get public hostname.
"""
if not self.interface_set.exclude(host=None):
return _('None')
if not self.primary_host:
return None
proto = 'ipv6' if use_ipv6 else 'ipv4'
return self.interface_set.exclude(host=None)[0].host.get_hostname(
proto=proto)
return self.primary_host.get_hostname(proto=proto)
def get_connect_command(self, use_ipv6=False):
"""Returns a formatted connect string.
......
......@@ -76,6 +76,11 @@ class InstanceOperation(Operation):
code_suffix=self.activity_code_suffix, instance=self.instance,
user=user, concurrency_check=self.concurrency_check)
def is_preferred(self):
"""If this is the recommended op in the current state of the instance.
"""
return False
class AddInterfaceOperation(InstanceOperation):
activity_code_suffix = 'add_interface'
......@@ -166,6 +171,10 @@ class DeployOperation(InstanceOperation):
if self.instance.status in ['RUNNING', 'SUSPENDED']:
raise self.instance.WrongStateError(self.instance)
def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED,
self.instance.STATUS.ERROR)
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
......@@ -377,6 +386,10 @@ class SaveAsTemplateOperation(InstanceOperation):
abortable = True
required_perms = ('vm.create_template', )
def is_preferred(self):
return (self.instance.is_base and
self.instance.status == self.instance.STATUS.RUNNING)
@staticmethod
def _rename(name):
m = search(r" v(\d+)$", name)
......@@ -525,6 +538,10 @@ class SleepOperation(InstanceOperation):
description = _("Suspend virtual machine with memory dump.")
required_perms = ()
def is_preferred(self):
return (not self.instance.is_base and
self.instance.status == self.instance.STATUS.RUNNING)
def check_precond(self):
super(SleepOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
......@@ -565,6 +582,10 @@ class WakeUpOperation(InstanceOperation):
""")
required_perms = ()
def is_preferred(self):
return (self.instance.is_base and
self.instance.status == self.instance.STATUS.SUSPENDED)
def check_precond(self):
super(WakeUpOperation, self).check_precond()
if self.instance.status not in ['SUSPENDED']:
......
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