Commit e4141022 by Őry Máté

Merge remote-tracking branch 'origin/feature-new-saml-group'

parents a499689c 94e3de4b
...@@ -96,3 +96,8 @@ for i in LOCAL_APPS: ...@@ -96,3 +96,8 @@ for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'DEBUG'} LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'DEBUG'}
CRISPY_FAIL_SILENTLY = not DEBUG CRISPY_FAIL_SILENTLY = not DEBUG
# propagate exceptions from signals
if DEBUG:
from django.dispatch import Signal
Signal.send_robust = Signal.send
...@@ -33,7 +33,7 @@ from crispy_forms.layout import ( ...@@ -33,7 +33,7 @@ from crispy_forms.layout import (
from crispy_forms.utils import render_field from crispy_forms.utils import render_field
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -44,7 +44,7 @@ from storage.models import Disk, DataStore ...@@ -44,7 +44,7 @@ from storage.models import Disk, DataStore
from vm.models import ( from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
) )
from .models import Profile from .models import Profile, GroupProfile
class VmCustomizeForm(forms.Form): class VmCustomizeForm(forms.Form):
...@@ -315,51 +315,80 @@ class VmCustomizeForm(forms.Form): ...@@ -315,51 +315,80 @@ class VmCustomizeForm(forms.Form):
class GroupCreateForm(forms.ModelForm): class GroupCreateForm(forms.ModelForm):
description = forms.CharField(label=_("Description"), required=False,
widget=forms.Textarea(attrs={'rows': 3}))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
new_groups = kwargs.pop('new_groups', None)
super(GroupCreateForm, self).__init__(*args, **kwargs) super(GroupCreateForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self) choices = [('', '--')]
self.helper.form_show_labels = False if new_groups:
self.helper.layout = Layout( choices += [(g, g) for g in new_groups if len(g) <= 64]
Div( self.fields['org_id'] = forms.ChoiceField(
Div( # TRANSLATORS: directory like in LDAP
AnyTag( choices=choices, required=False, label=_('Directory identifier'))
'h4', if not new_groups:
HTML(_("Name")), self.fields['org_id'].widget = HiddenInput()
),
css_class="col-sm-10",
),
css_class="row",
),
Div(
Div(
Field('name', id="group-create-name"),
css_class="col-sm-10",
),
css_class="row",
),
Div( # buttons def save(self, commit=True):
Div( if not commit:
AnyTag( # tip: don't try to use Button class raise AttributeError('Committing is mandatory.')
"button", group = super(GroupCreateForm, self).save()
AnyTag(
"i",
css_class="icon-play"
),
HTML(" Create"),
css_id="vm-create-submit",
css_class="btn btn-success",
), profile = group.profile
css_class="col-sm-5", # multiple blanks were not be unique unlike NULLs are
), profile.org_id = self.cleaned_data['org_id'] or None
css_class="row", profile.description = self.cleaned_data['description']
), profile.save()
)
return group
@property
def helper(self):
helper = FormHelper(self)
helper.add_input(Submit("submit", _("Create")))
helper.form_tag = False
return helper
class Meta: class Meta:
model = Group model = Group
fields = ['name', ] fields = ('name', )
class GroupProfileUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
new_groups = kwargs.pop('new_groups', None)
superuser = kwargs.pop('superuser', False)
super(GroupProfileUpdateForm, self).__init__(*args, **kwargs)
if not superuser:
choices = [('', '--')]
if new_groups:
choices += [(g, g) for g in new_groups if len(g) <= 64]
self.fields['org_id'] = forms.ChoiceField(
choices=choices, required=False,
label=_('Directory identifier'))
if not new_groups:
self.fields['org_id'].widget = HiddenInput()
self.fields['description'].widget = forms.Textarea(attrs={'rows': 3})
@property
def helper(self):
helper = FormHelper(self)
helper.add_input(Submit("submit", _("Save")))
helper.form_tag = False
return helper
def save(self, commit=True):
profile = super(GroupProfileUpdateForm, self).save(commit=False)
profile.org_id = self.cleaned_data['org_id'] or None
if commit:
profile.save()
return profile
class Meta:
model = GroupProfile
fields = ('description', 'org_id')
class HostForm(forms.ModelForm): class HostForm(forms.ModelForm):
......
...@@ -104,7 +104,12 @@ class GroupProfile(AclBase): ...@@ -104,7 +104,12 @@ class GroupProfile(AclBase):
org_id = CharField( org_id = CharField(
unique=True, blank=True, null=True, max_length=64, unique=True, blank=True, null=True, max_length=64,
help_text=_('Unique identifier of the group at the organization.')) help_text=_('Unique identifier of the group at the organization.'))
description = TextField() description = TextField(blank=True)
def save(self, *args, **kwargs):
if not self.org_id:
self.org_id = None
super(GroupProfile, self).save(*args, **kwargs)
@classmethod @classmethod
def search(cls, name): def search(cls, name):
...@@ -162,7 +167,8 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -162,7 +167,8 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
logger.debug("org_id of %s already added to user %s's profile", logger.debug("org_id of %s already added to user %s's profile",
value, sender.username) value, sender.username)
memberatrs = getattr(settings, 'SAML_GROUP_ATTRIBUTES', []) memberatrs = getattr(settings, 'SAML_GROUP_ATTRIBUTES', [])
for group in chain(*[attributes[i] for i in memberatrs]): for group in chain(*[attributes[i]
for i in memberatrs if i in attributes]):
try: try:
g = GroupProfile.search(group) g = GroupProfile.search(group)
except Group.DoesNotExist: except Group.DoesNotExist:
...@@ -173,7 +179,8 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -173,7 +179,8 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
g.user_set.add(sender) g.user_set.add(sender)
owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', []) owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
for group in chain(*[attributes[i] for i in owneratrs]): for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]):
try: try:
g = GroupProfile.search(group) g = GroupProfile.search(group)
except Group.DoesNotExist: except Group.DoesNotExist:
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
} }
</style> </style>
<form method="POST" action="/dashboard/group/create/"> <form method="POST" action="{% url "dashboard.views.group-create" %}">
{% csrf_token %} {% csrf_token %}
{% crispy form %} {% crispy form %}
</form> </form>
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load crispy_forms_tags %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
...@@ -19,6 +20,9 @@ ...@@ -19,6 +20,9 @@
</div> </div>
<div id="group-details-h1-name"> <div id="group-details-h1-name">
{{ group.name }} {{ group.name }}
{% if group.groupprofile.org_id %}
<small>{{group.groupprofile.org_id}}</small>
{% endif %}
</div> </div>
</h1> </h1>
<div class="group-details-help js-hidden"> <div class="group-details-help js-hidden">
...@@ -38,6 +42,12 @@ ...@@ -38,6 +42,12 @@
<div class="col-md-12" id="group-detail-pane"> <div class="col-md-12" id="group-detail-pane">
<div class="panel panel-default" id="group-detail-panel"> <div class="panel panel-default" id="group-detail-panel">
<div class="tab-content panel-body" id="group-form-body"> <div class="tab-content panel-body" id="group-form-body">
<form method="POST" action="{% url "dashboard.views.group-update" pk=group.pk %}">
{% csrf_token %}
{% crispy group_profile_form %}
</form>
<h3>{% trans "User list"|capfirst %}</h3> <h3>{% trans "User list"|capfirst %}</h3>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table"> <table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table">
......
{% extends "dashboard/base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default" style="margin-top: 60px;">
<div class="panel-heading">
<h3 class="no-margin">
{% trans "Update group" %}
</h3>
</div>
<div class="panel-body">
<form method="POST" action="">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
</div>
{% endblock %}
...@@ -31,7 +31,7 @@ from .views import ( ...@@ -31,7 +31,7 @@ from .views import (
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView, VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView, GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupCreate, GroupCreate, GroupProfileUpdate,
TemplateChoose, TemplateChoose,
UserCreationView, UserCreationView,
get_vm_screenshot, get_vm_screenshot,
...@@ -121,6 +121,8 @@ urlpatterns = patterns( ...@@ -121,6 +121,8 @@ urlpatterns = patterns(
name='dashboard.views.node-graph'), name='dashboard.views.node-graph'),
url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(), url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(),
name='dashboard.views.group-detail'), name='dashboard.views.group-detail'),
url(r'^group/(?P<pk>\d+)/update/$', GroupProfileUpdate.as_view(),
name='dashboard.views.group-update'),
url(r'^group/(?P<pk>\d+)/acl/$', GroupAclUpdateView.as_view(), url(r'^group/(?P<pk>\d+)/acl/$', GroupAclUpdateView.as_view(),
name='dashboard.views.group-acl'), name='dashboard.views.group-acl'),
url(r'^notifications/$', NotificationView.as_view(), url(r'^notifications/$', NotificationView.as_view(),
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from itertools import chain
from os import getenv from os import getenv
import json import json
import logging import logging
...@@ -58,7 +59,7 @@ from braces.views._access import AccessMixin ...@@ -58,7 +59,7 @@ from braces.views._access import AccessMixin
from .forms import ( from .forms import (
CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm, CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm,
NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm, NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
UserCreationForm, UserCreationForm, GroupProfileUpdateForm,
CirclePasswordChangeForm CirclePasswordChangeForm
) )
...@@ -75,6 +76,7 @@ from firewall.models import Vlan, Host, Rule ...@@ -75,6 +76,7 @@ from firewall.models import Vlan, Host, Rule
from .models import Favourite, Profile, GroupProfile from .models import Favourite, Profile, GroupProfile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
saml_available = hasattr(settings, "SAML_CONFIG")
def search_user(keyword): def search_user(keyword):
...@@ -87,6 +89,39 @@ def search_user(keyword): ...@@ -87,6 +89,39 @@ def search_user(keyword):
return User.objects.get(email=keyword) return User.objects.get(email=keyword)
class GroupCodeMixin(object):
@classmethod
def get_available_group_codes(cls, request):
newgroups = []
if saml_available:
from djangosaml2.cache import StateCache, IdentityCache
from djangosaml2.conf import get_config
from djangosaml2.views import _get_subject_id
from saml2.client import Saml2Client
state = StateCache(request.session)
conf = get_config(None, request)
client = Saml2Client(conf, state_cache=state,
identity_cache=IdentityCache(request.session),
logger=logger)
subject_id = _get_subject_id(request.session)
identity = client.users.get_identity(subject_id,
check_not_on_or_after=False)
if identity:
attributes = identity[0]
owneratrs = getattr(
settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]):
try:
GroupProfile.search(group)
except Group.DoesNotExist:
newgroups.append(group)
return newgroups
class FilterMixin(object): class FilterMixin(object):
def get_queryset_filters(self): def get_queryset_filters(self):
...@@ -692,6 +727,8 @@ class GroupDetailView(CheckedDetailView): ...@@ -692,6 +727,8 @@ class GroupDetailView(CheckedDetailView):
context['group'] = self.object context['group'] = self.object
context['users'] = self.object.user_set.all() context['users'] = self.object.user_set.all()
context['acl'] = get_group_acl_data(self.object) context['acl'] = get_group_acl_data(self.object)
context['group_profile_form'] = GroupProfileUpdate.get_form_object(
self.request, self.object.profile)
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
...@@ -1565,10 +1602,9 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): ...@@ -1565,10 +1602,9 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
return redirect(path) return redirect(path)
class GroupCreate(LoginRequiredMixin, TemplateView): class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
form_class = GroupCreateForm form_class = GroupCreateForm
form = None
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
...@@ -1580,7 +1616,8 @@ class GroupCreate(LoginRequiredMixin, TemplateView): ...@@ -1580,7 +1616,8 @@ class GroupCreate(LoginRequiredMixin, TemplateView):
if not request.user.has_module_perms('auth'): if not request.user.has_module_perms('auth'):
raise PermissionDenied() raise PermissionDenied()
if form is None: if form is None:
form = self.form_class() form = self.form_class(
new_groups=self.get_available_group_codes(request))
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
context.update({ context.update({
'template': 'dashboard/group-create.html', 'template': 'dashboard/group-create.html',
...@@ -1593,7 +1630,8 @@ class GroupCreate(LoginRequiredMixin, TemplateView): ...@@ -1593,7 +1630,8 @@ class GroupCreate(LoginRequiredMixin, TemplateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not request.user.has_module_perms('auth'): if not request.user.has_module_perms('auth'):
raise PermissionDenied() raise PermissionDenied()
form = self.form_class(request.POST) form = self.form_class(
request.POST, new_groups=self.get_available_group_codes(request))
if not form.is_valid(): if not form.is_valid():
return self.get(request, form, *args, **kwargs) return self.get(request, form, *args, **kwargs)
form.cleaned_data form.cleaned_data
...@@ -1608,6 +1646,55 @@ class GroupCreate(LoginRequiredMixin, TemplateView): ...@@ -1608,6 +1646,55 @@ class GroupCreate(LoginRequiredMixin, TemplateView):
return redirect(savedform.profile.get_absolute_url()) return redirect(savedform.profile.get_absolute_url())
class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
LoginRequiredMixin, UpdateView):
form_class = GroupProfileUpdateForm
model = Group
success_message = _('Group is successfully updated.')
@classmethod
def get_available_group_codes(cls, request, extra=None):
result = super(GroupProfileUpdate, cls).get_available_group_codes(
request)
if extra and extra not in result:
result += [extra]
return result
def get_object(self):
group = super(GroupProfileUpdate, self).get_object()
profile = group.profile
if not profile.has_level(self.request.user, 'owner'):
raise PermissionDenied
else:
return profile
@classmethod
def get_form_object(cls, request, instance, *args, **kwargs):
kwargs['instance'] = instance
kwargs['new_groups'] = cls.get_available_group_codes(
request, instance.org_id)
kwargs['superuser'] = request.user.is_superuser
return cls.form_class(*args, **kwargs)
def get(self, request, form=None, *args, **kwargs):
self.object = self.get_object()
if form is None:
form = self.get_form_object(request, self.object)
return super(GroupProfileUpdate, self).get(
request, form, *args, **kwargs)
def post(self, request, *args, **kwargs):
if not request.user.has_module_perms('auth'):
raise PermissionDenied()
self.object = self.get_object()
form = self.get_form_object(request, self.object, self.request.POST)
if not form.is_valid():
return self.form_invalid(form)
form.save()
return self.form_valid(form)
class VmDelete(LoginRequiredMixin, DeleteView): class VmDelete(LoginRequiredMixin, DeleteView):
model = Instance model = Instance
template_name = "dashboard/confirm/base-delete.html" template_name = "dashboard/confirm/base-delete.html"
...@@ -1880,9 +1967,9 @@ class VmMassDelete(LoginRequiredMixin, View): ...@@ -1880,9 +1967,9 @@ class VmMassDelete(LoginRequiredMixin, View):
logger.info('Tried to delete instance #%d without owner ' logger.info('Tried to delete instance #%d without owner '
'permission by %s.', i.pk, 'permission by %s.', i.pk,
unicode(request.user)) unicode(request.user))
raise PermissionDenied() # no need for rollback or proper # no need for rollback or proper error message, this can't
# error message, this can't # normally happen:
# normally happen. raise PermissionDenied()
try: try:
i.destroy.async(user=request.user) i.destroy.async(user=request.user)
names.append(i.name) names.append(i.name)
...@@ -2416,7 +2503,7 @@ class NotificationView(LoginRequiredMixin, TemplateView): ...@@ -2416,7 +2503,7 @@ class NotificationView(LoginRequiredMixin, TemplateView):
def circle_login(request): def circle_login(request):
authentication_form = CircleAuthenticationForm authentication_form = CircleAuthenticationForm
extra_context = { extra_context = {
'saml2': hasattr(settings, "SAML_CONFIG") 'saml2': saml_available,
} }
response = login(request, authentication_form=authentication_form, response = login(request, authentication_form=authentication_form,
extra_context=extra_context) extra_context=extra_context)
......
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