Merge branch 'feature/set_sftp_password'

* feature/set_sftp_password:
  plug users and hosting packages together
  implement CustomerHostingPackageDetails view
  introduce new settings for groups and upload server
  implement osusers.forms.ChangeOsUserPasswordForm
  refactor dashboard.views.UserDashboardView
  generate documentation for gvacommon.viewmixins
  implement viewmixins.StaffOrSelfLoginRequiredMixin
This commit is contained in:
Jan Dittberner 2015-01-24 16:32:12 +01:00
commit f55886f1fe
20 changed files with 379 additions and 31 deletions

View file

@ -1,6 +1,8 @@
Changelog Changelog
========= =========
* :feature:`-` add frontend functionality to set an os users' sftp password
(needs gvaldap >= 0.4.0 on the LDAP side)
* :support:`-` remove unused dashboard.views.LogoutView and the corresponding * :support:`-` remove unused dashboard.views.LogoutView and the corresponding
URL in dashboard.urls URL in dashboard.urls
* :feature:`-` add new task stub to set an ldap user's password * :feature:`-` add new task stub to set an ldap user's password

View file

@ -13,3 +13,11 @@ provides some functionality that is common to all gnuviechadmin subprojects.
.. automodule:: gvacommon.celeryrouters .. automodule:: gvacommon.celeryrouters
:members: :members:
:undoc-members: :undoc-members:
:py:mod:`viewmixins <gvacommon.viewmixins>`
-------------------------------------------
.. automodule:: gvacommon.viewmixins
:members:
:undoc-members:

View file

@ -22,3 +22,17 @@
.. automodule:: hostingpackages.models .. automodule:: hostingpackages.models
:members: :members:
:py:mod:`views <hostingpackages.views>`
---------------------------------------
.. automodule:: hostingpackages.views
:members:
:py:mod:`urls <hostingpackages.urls>`
-------------------------------------
.. automodule:: hostingpackages.urls
:members:

View file

@ -18,8 +18,28 @@
:members: :members:
:py:mod:`forms <osusers.forms>`
-------------------------------
.. automodule:: osusers.forms
:members:
:py:mod:`models <osusers.models>` :py:mod:`models <osusers.models>`
--------------------------------- ---------------------------------
.. automodule:: osusers.models .. automodule:: osusers.models
:members: :members:
:py:mod:`urls <osusers.urls>`
-----------------------------
.. automodule:: osusers.urls
:py:mod:`views <osusers.views>`
-------------------------------
.. automodule:: osusers.views
:members:

View file

@ -4,15 +4,13 @@ This module defines the views for the gnuviechadmin customer dashboard.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.http import HttpResponseForbidden
from django.views.generic import ( from django.views.generic import (
DetailView, DetailView,
TemplateView, TemplateView,
) )
from django.utils.translation import ugettext as _
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from braces.views import LoginRequiredMixin from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin
from hostingpackages.models import CustomerHostingPackage from hostingpackages.models import CustomerHostingPackage
@ -25,7 +23,7 @@ class IndexView(TemplateView):
template_name = 'dashboard/index.html' template_name = 'dashboard/index.html'
class UserDashboardView(LoginRequiredMixin, DetailView): class UserDashboardView(StaffOrSelfLoginRequiredMixin, DetailView):
""" """
This is the user dashboard view. This is the user dashboard view.
@ -35,18 +33,16 @@ class UserDashboardView(LoginRequiredMixin, DetailView):
slug_field = 'username' slug_field = 'username'
template_name = 'dashboard/user_dashboard.html' template_name = 'dashboard/user_dashboard.html'
def dispatch(self, request, *args, **kwargs):
if (request.user.is_staff or request.user == self.get_object()):
return super(UserDashboardView, self).dispatch(
request, *args, **kwargs
)
return HttpResponseForbidden(
_('You are not allowed to view this page.')
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(UserDashboardView, self).get_context_data(**kwargs) context = super(UserDashboardView, self).get_context_data(**kwargs)
context['hosting_packages'] = CustomerHostingPackage.objects.filter( context['hosting_packages'] = CustomerHostingPackage.objects.filter(
customer=self.object customer=self.object
) )
return context return context
def get_customer_object(self):
"""
Returns the customer object.
"""
return self.get_object()

View file

@ -352,5 +352,8 @@ OSUSER_MINGID = int(get_env_variable('GVA_MIN_OS_GID'))
OSUSER_USERNAME_PREFIX = get_env_variable('GVA_OSUSER_PREFIX') OSUSER_USERNAME_PREFIX = get_env_variable('GVA_OSUSER_PREFIX')
OSUSER_HOME_BASEPATH = get_env_variable('GVA_OSUSER_HOME_BASEPATH') OSUSER_HOME_BASEPATH = get_env_variable('GVA_OSUSER_HOME_BASEPATH')
OSUSER_DEFAULT_SHELL = get_env_variable('GVA_OSUSER_DEFAULT_SHELL') OSUSER_DEFAULT_SHELL = get_env_variable('GVA_OSUSER_DEFAULT_SHELL')
OSUSER_DEFAULT_GROUPS = ['sftponly'] OSUSER_SFTP_GROUP = 'sftponly'
OSUSER_SSH_GROUP = 'sshusers'
OSUSER_DEFAULT_GROUPS = [OSUSER_SFTP_GROUP]
OSUSER_UPLOAD_SERVER = get_env_variable('GVA_OSUSER_UPLOADSERVER')
########## END CUSTOM APP CONFIGURATION ########## END CUSTOM APP CONFIGURATION

View file

@ -11,6 +11,7 @@ urlpatterns = patterns(
url(r'', include('dashboard.urls')), url(r'', include('dashboard.urls')),
url(r'^accounts/', include('allauth.urls')), url(r'^accounts/', include('allauth.urls')),
url(r'^hosting/', include('hostingpackages.urls')), url(r'^hosting/', include('hostingpackages.urls')),
url(r'^osuser/', include('osusers.urls')),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
) )

View file

@ -0,0 +1,42 @@
"""
This module defines mixins for gnuviechadmin views.
"""
from __future__ import unicode_literals
from django.http import HttpResponseForbidden
from django.utils.translation import ugettext as _
from braces.views import LoginRequiredMixin
class StaffOrSelfLoginRequiredMixin(LoginRequiredMixin):
"""
Mixin that makes sure that a user is logged in and matches the current
customer or is a staff user.
"""
def dispatch(self, request, *args, **kwargs):
if (
request.user.is_staff or
request.user == self.get_customer_object()
):
return super(StaffOrSelfLoginRequiredMixin, self).dispatch(
request, *args, **kwargs
)
return HttpResponseForbidden(
_('You are not allowed to view this page.')
)
def get_customer_object(self):
"""
Views based on this mixin have to implement this method to return
the customer that must be an object of the same class as the
django.contrib.auth user type.
:return: customer
:rtype: settings.AUTH_USER_MODEL
"""
raise NotImplemented("subclass has to implement get_customer_object")

View file

@ -5,6 +5,7 @@ This module contains the hosting package models.
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
@ -218,6 +219,12 @@ class CustomerHostingPackage(HostingPackageBase):
name=self.name, customer=self.customer name=self.name, customer=self.customer
) )
def get_absolute_url(self):
return reverse('hosting_package_details', kwargs={
'user': self.customer.username,
'pk': self.id,
})
def copy_template_attributes(self): def copy_template_attributes(self):
""" """
Copy the attributes of the hosting package's template to the package. Copy the attributes of the hosting package's template to the package.

View file

@ -1,12 +1,22 @@
"""
This module defines the URL patterns for hosting package related views.
"""
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from .views import CreateHostingPackage from .views import (
CreateHostingPackage,
CustomerHostingPackageDetails,
)
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^(?P<user>[\w0-9@.+-_]+)/create$', CreateHostingPackage.as_view(), url(r'^(?P<user>[\w0-9@.+-_]+)/create$', CreateHostingPackage.as_view(),
name='create_hosting_package'), name='create_hosting_package'),
url(r'^(?P<user>[\w0-9@.+-_]+)/hostingpackage/(?P<pk>\d+)/$',
CustomerHostingPackageDetails.as_view(),
name='hosting_package_details'),
) )

View file

@ -4,18 +4,22 @@ This module defines views related to hosting packages.
""" """
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import DetailView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from django.contrib.auth import get_user_model
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model
from braces.views import ( from braces.views import (
LoginRequiredMixin, LoginRequiredMixin,
StaffuserRequiredMixin, StaffuserRequiredMixin,
) )
from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin
from .forms import CreateHostingPackageForm from .forms import CreateHostingPackageForm
from .models import CustomerHostingPackage from .models import CustomerHostingPackage
@ -37,13 +41,12 @@ class CreateHostingPackage(
kwargs.update(self.kwargs) kwargs.update(self.kwargs)
return kwargs return kwargs
def _get_customer(self): def get_customer_object(self):
return get_user_model().objects.get(username=self.kwargs['user']) return get_user_model().objects.get(username=self.kwargs['user'])
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CreateHostingPackage, self).get_context_data(**kwargs) context = super(CreateHostingPackage, self).get_context_data(**kwargs)
context['customer'] = self._get_customer() context['customer'] = self.get_customer_object()
return context return context
def get_success_url(self): def get_success_url(self):
@ -52,7 +55,7 @@ class CreateHostingPackage(
def form_valid(self, form): def form_valid(self, form):
hostingpackage = form.save(commit=False) hostingpackage = form.save(commit=False)
hostingpackage.customer = self._get_customer() hostingpackage.customer = self.get_customer_object()
hostingpackage.save() hostingpackage.save()
messages.success( messages.success(
self.request, self.request,
@ -60,3 +63,24 @@ class CreateHostingPackage(
name=hostingpackage.name) name=hostingpackage.name)
) )
return redirect(self.get_success_url()) return redirect(self.get_success_url())
class CustomerHostingPackageDetails(StaffOrSelfLoginRequiredMixin, DetailView):
"""
This view is for showing details of a customer hosting package.
"""
model = CustomerHostingPackage
context_object_name = 'hostingpackage'
def get_customer_object(self):
return get_user_model().objects.get(username=self.kwargs['user'])
def get_context_data(self, **kwargs):
context = super(CustomerHostingPackageDetails, self).get_context_data(
**kwargs)
context.update({
'customer': self.get_customer_object(),
'uploadserver': settings.OSUSER_UPLOAD_SERVER,
})
return context

View file

@ -6,6 +6,9 @@ from django import forms
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib import admin from django.contrib import admin
from .forms import (
PASSWORD_MISMATCH_ERROR
)
from .models import ( from .models import (
AdditionalGroup, AdditionalGroup,
Group, Group,
@ -13,11 +16,6 @@ from .models import (
User, User,
) )
PASSWORD_MISMATCH_ERROR = _("Passwords don't match")
"""
Error message for non matching passwords.
"""
class AdditionalGroupInline(admin.TabularInline): class AdditionalGroupInline(admin.TabularInline):
""" """

View file

@ -0,0 +1,71 @@
"""
This module defines operating system user related forms.
"""
from __future__ import unicode_literals
from django import forms
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from .models import User
PASSWORD_MISMATCH_ERROR = _("Passwords don't match")
"""
Error message for non matching passwords.
"""
class ChangeOsUserPasswordForm(forms.ModelForm):
"""
A form for setting an OS user's password.
"""
password1 = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
required=False,
)
password2 = forms.CharField(
label=_('Password (again)'), widget=forms.PasswordInput,
required=False,
)
class Meta:
model = User
fields = []
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
super(ChangeOsUserPasswordForm, self).__init__(*args, **kwargs)
self.helper.form_action = reverse(
'set_osuser_password', kwargs={'slug': self.instance.username})
self.helper.add_input(Submit('submit', _('Set password')))
def clean_password2(self):
"""
Check that the two password entries match.
:return: the validated password
:rtype: str or None
"""
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError(PASSWORD_MISMATCH_ERROR)
return password2
def save(self, commit=True):
"""
Save the provided password in hashed format.
:param boolean commit: whether to save the created user
:return: user instance
:rtype: :py:class:`osusers.models.User`
"""
self.instance.set_password(self.cleaned_data['password1'])
return super(ChangeOsUserPasswordForm, self).save(commit=commit)

View file

@ -29,6 +29,7 @@ from ldaptasks.tasks import (
delete_ldap_group, delete_ldap_group,
delete_ldap_user, delete_ldap_user,
remove_ldap_user_from_group, remove_ldap_user_from_group,
set_ldap_user_password,
) )
from fileservertasks.tasks import ( from fileservertasks.tasks import (
@ -245,15 +246,26 @@ class User(TimeStampedModel, models.Model):
""" """
if hasattr(self, 'shadow'): if hasattr(self, 'shadow'):
self.shadow.set_password(password) self.shadow.set_password(password)
success = set_ldap_user_password.delay(
self.username, password).get()
if success:
_LOGGER.info(
"successfully set LDAP password for %s", self.username)
else:
_LOGGER.error(
"setting the LDAP password for %s failed", self.username)
return success
else: else:
self.shadow = Shadow.objects.create_shadow( self.shadow = Shadow.objects.create_shadow(
user=self, password=password user=self, password=password
) )
dn = create_ldap_user.delay( dn = create_ldap_user.delay(
self.username, self.uid, self.group.gid, self.gecos, self.username, self.uid, self.group.gid, self.gecos,
self.homedir, self.shell, password self.homedir, self.shell, password
).get() ).get()
logging.info("set LDAP password for %s", dn) _LOGGER.info("set LDAP password for %s", dn)
return True
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View file

@ -0,0 +1,16 @@
"""
This module defines the URL patterns for operating system user related views.
"""
from __future__ import absolute_import, unicode_literals
from django.conf.urls import patterns, url
from .views import SetOsUserPassword
urlpatterns = patterns(
'',
url(r'^(?P<slug>[\w0-9@.+-_]+)/setpassword$', SetOsUserPassword.as_view(),
name='set_osuser_password'),
)

View file

@ -0,0 +1,45 @@
"""
This module defines the views for gnuviechadmin operating system user handling.
"""
from __future__ import unicode_literals, absolute_import
from django.shortcuts import redirect
from django.views.generic import UpdateView
from django.utils.translation import ugettext as _
from django.contrib import messages
from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin
from .forms import ChangeOsUserPasswordForm
from .models import User
class SetOsUserPassword(StaffOrSelfLoginRequiredMixin, UpdateView):
"""
This view is used for setting a new operating system user password.
"""
model = User
slug_field = 'username'
template_name_suffix = '_setpassword'
context_object_name = 'osuser'
form_class = ChangeOsUserPasswordForm
def get_customer_object(self):
return self.get_object().customer
def get_context_data(self, *args, **kwargs):
context = super(SetOsUserPassword, self).get_context_data(
*args, **kwargs)
context['customer'] = self.get_customer_object()
return context
def form_valid(self, form):
osuser = form.save()
messages.success(
self.request,
_("New password for {username} has been set successfully.").format(
username=osuser.username
))
return redirect(osuser.customerhostingpackage)

View file

@ -22,7 +22,7 @@
<tbody> <tbody>
{% for package in hosting_packages %} {% for package in hosting_packages %}
<tr> <tr>
<th>{{ package.name }}</th> <th><a href="{{ package.get_absolute_url }}" title="{% blocktrans with packagename=package.name %}Show details for {{ packagename }}{% endblocktrans %}">{{ package.name }}</a></th>
<th> <th>
{% with diskspace=package.get_disk_space %} {% with diskspace=package.get_disk_space %}
<span title="{% blocktrans %}The reserved disk space for your hosting package is {{ diskspace }} bytes.{% endblocktrans %}">{{ diskspace|filesizeformat }}</span> <span title="{% blocktrans %}The reserved disk space for your hosting package is {{ diskspace }} bytes.{% endblocktrans %}">{{ diskspace|filesizeformat }}</span>

View file

@ -0,0 +1,48 @@
{% extends "hostingpackages/base.html" %}
{% load i18n %}
{% block title %}{{ block.super }} - {% spaceless %}
{% if user == customer %}
{% blocktrans with package=hostingpackage.name %}Details for your Hosting Package {{ package }}{% endblocktrans %}
{% else %}
{% blocktrans with package=hostingpackage.name full_name=customer.get_full_name %}Details for Hosting Package {{ package }} of {{ full_name }}{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock title %}
{% block page_title %}{% blocktrans with package=hostingpackage.name %}Details of Hosting Package {{ package }}{% endblocktrans %}{% endblock page_title %}
{% block content %}
<div class="row">
<div class="col-lg-6 col-md-6 col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
{% trans "Hosting Package Information" %}<div class="pull-right"><a class="panel-title" href="#" title="{% trans "Edit Hosting Package Information" %}"><i class="glyphicon glyphicon-cog"></i></a></div>
</div>
<dl class="panel-body dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd>{{ hostingpackage.name }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ hostingpackage.description|default:"-" }}</dd>
<dt>{% trans "Disk space" %}</dt>
{% with diskspace=hostingpackage.get_disk_space %}
<dd title="{% blocktrans %}The reserved disk space for your hosting package is {{ diskspace }} bytes.{% endblocktrans %}">{{ diskspace|filesizeformat }}</dd>
{% endwith %}
<dt>{% trans "Mailboxes" %}</dt>
<dd>{% blocktrans with num=hostingpackage.get_used_mailboxes total=hostingpackage.get_mailboxes %}{{ num }} of {{ total }} in use{% endblocktrans %}</dd>
<dt>{% if hostingpackage.osuser.is_sftp_user %}{% trans "SFTP username" %}{% else %}{% trans "SSH/SFTP username" %}{% endif %}</dt>
<dd>{{ hostingpackage.osuser.username }}</dd>
<dt>{% trans "Upload server" %}</dt>
<dd>{{ uploadserver }}</dd>
</dl>
</div>
</div>
<div class="col-lg-6 col-md-6 col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">{% trans "Hosting Package Actions" %}</div>
<ul class="list-group">
<li class="list-group-item"><a href="{% url "set_osuser_password" slug=hostingpackage.osuser.username %}">{% if hostingpackage.osuser.is_sftp %}{% trans "Set SFTP password" %}{% else %}{% trans "Set SSH/SFTP password" %}{% endif %}</a></li>
</ul>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1 @@
{% extends "base.html" %}

View file

@ -0,0 +1,30 @@
{% extends "osusers/base.html" %}
{% load i18n crispy_forms_tags %}
{% block title %}{{ block.super }} - {% spaceless %}
{% if customer == user %}
{% blocktrans with osuser=osuser.username %}Set new password for user {{ osuser }}{% endblocktrans %}
{% else %}
{% blocktrans with osuser=osuser.username full_name=customer.get_full_name %}Set new password for user {{ osuser }} of customer {{ full_name }}{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock title %}
{% block page_title %}{% spaceless %}
{% if customer == user %}
{% blocktrans with osuser=osuser.username %}Set new password for user {{ osuser }}{% endblocktrans %}
{% else %}
{% blocktrans with osuser=osuser.username full_name=customer.get_full_name %}Set new password for user {{ osuser }} of customer {{ full_name }}{% endblocktrans %}
{% endif %}
{% endspaceless %}{% endblock page_title %}
{% block content %}
{% crispy form %}
{% endblock content %}
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function() {
$('input[type=password]').val('');
$('input[type=password]').first().focus();
});
</script>
{% endblock extra_js %}