From 3c4d34cce56dfb75e0e4115c3938ce5b2e6efd83 Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sat, 24 Jan 2015 15:38:08 +0100
Subject: [PATCH 1/7] implement viewmixins.StaffOrSelfLoginRequiredMixin

---
 viewmixins.py | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)
 create mode 100644 viewmixins.py

diff --git a/viewmixins.py b/viewmixins.py
new file mode 100644
index 0000000..fc7f106
--- /dev/null
+++ b/viewmixins.py
@@ -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")

From dd7a40a019c81d90bd9e84648a066d7e3d54970e Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sat, 24 Jan 2015 15:42:20 +0100
Subject: [PATCH 2/7] generate documentation for gvacommon.viewmixins

---
 docs/code/gvacommon.rst | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/docs/code/gvacommon.rst b/docs/code/gvacommon.rst
index 4dbc502..4a16cb2 100644
--- a/docs/code/gvacommon.rst
+++ b/docs/code/gvacommon.rst
@@ -13,3 +13,11 @@ provides some functionality that is common to all gnuviechadmin subprojects.
 .. automodule:: gvacommon.celeryrouters
    :members:
    :undoc-members:
+
+
+:py:mod:`viewmixins <gvacommon.viewmixins>`
+-------------------------------------------
+
+.. automodule:: gvacommon.viewmixins
+   :members:
+   :undoc-members:

From 3a9110dc30eeb97baf4d52a87701a1628d6633fd Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sat, 24 Jan 2015 16:12:23 +0100
Subject: [PATCH 3/7] refactor dashboard.views.UserDashboardView

- use gvacommon.viewmixins.StaffOrSelfLoginRequiredMixin instead of custom
  implementation
---
 gnuviechadmin/dashboard/views.py | 22 +++++++++-------------
 1 file changed, 9 insertions(+), 13 deletions(-)

diff --git a/gnuviechadmin/dashboard/views.py b/gnuviechadmin/dashboard/views.py
index d378b1d..1542e45 100644
--- a/gnuviechadmin/dashboard/views.py
+++ b/gnuviechadmin/dashboard/views.py
@@ -4,15 +4,13 @@ This module defines the views for the gnuviechadmin customer dashboard.
 """
 from __future__ import unicode_literals
 
-from django.http import HttpResponseForbidden
 from django.views.generic import (
     DetailView,
     TemplateView,
 )
-from django.utils.translation import ugettext as _
 from django.contrib.auth import get_user_model
 
-from braces.views import LoginRequiredMixin
+from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin
 
 from hostingpackages.models import CustomerHostingPackage
 
@@ -25,7 +23,7 @@ class IndexView(TemplateView):
     template_name = 'dashboard/index.html'
 
 
-class UserDashboardView(LoginRequiredMixin, DetailView):
+class UserDashboardView(StaffOrSelfLoginRequiredMixin, DetailView):
     """
     This is the user dashboard view.
 
@@ -35,18 +33,16 @@ class UserDashboardView(LoginRequiredMixin, DetailView):
     slug_field = 'username'
     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):
         context = super(UserDashboardView, self).get_context_data(**kwargs)
         context['hosting_packages'] = CustomerHostingPackage.objects.filter(
             customer=self.object
         )
         return context
+
+    def get_customer_object(self):
+        """
+        Returns the customer object.
+
+        """
+        return self.get_object()

From 68c0bfbb4e882313fbc2b8b3200629c4147cb9a5 Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sat, 24 Jan 2015 16:15:42 +0100
Subject: [PATCH 4/7] implement osusers.forms.ChangeOsUserPasswordForm

- implement new form for password changes
- use osusers.forms.PASSWORD_MISMATCH_ERROR in osusers.admin
- add autogenerated documentation
---
 docs/code/osusers.rst          |  7 ++++
 gnuviechadmin/osusers/admin.py |  8 ++--
 gnuviechadmin/osusers/forms.py | 71 ++++++++++++++++++++++++++++++++++
 3 files changed, 81 insertions(+), 5 deletions(-)
 create mode 100644 gnuviechadmin/osusers/forms.py

diff --git a/docs/code/osusers.rst b/docs/code/osusers.rst
index 46d72cb..79b7e05 100644
--- a/docs/code/osusers.rst
+++ b/docs/code/osusers.rst
@@ -18,6 +18,13 @@
    :members:
 
 
+:py:mod:`forms <osusers.forms>`
+-------------------------------
+
+.. automodule:: osusers.forms
+   :members:
+
+
 :py:mod:`models <osusers.models>`
 ---------------------------------
 
diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py
index 3bb3579..7d757be 100644
--- a/gnuviechadmin/osusers/admin.py
+++ b/gnuviechadmin/osusers/admin.py
@@ -6,6 +6,9 @@ from django import forms
 from django.utils.translation import ugettext as _
 from django.contrib import admin
 
+from .forms import (
+    PASSWORD_MISMATCH_ERROR
+)
 from .models import (
     AdditionalGroup,
     Group,
@@ -13,11 +16,6 @@ from .models import (
     User,
 )
 
-PASSWORD_MISMATCH_ERROR = _("Passwords don't match")
-"""
-Error message for non matching passwords.
-"""
-
 
 class AdditionalGroupInline(admin.TabularInline):
     """
diff --git a/gnuviechadmin/osusers/forms.py b/gnuviechadmin/osusers/forms.py
new file mode 100644
index 0000000..1ed1c82
--- /dev/null
+++ b/gnuviechadmin/osusers/forms.py
@@ -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)

From 0baee51d19af9b42711f6127e26cb4151f68225f Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sat, 24 Jan 2015 16:17:39 +0100
Subject: [PATCH 5/7] introduce new settings for groups and upload server

---
 gnuviechadmin/gnuviechadmin/settings/base.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py
index 3237cb2..6d52afb 100644
--- a/gnuviechadmin/gnuviechadmin/settings/base.py
+++ b/gnuviechadmin/gnuviechadmin/settings/base.py
@@ -352,5 +352,8 @@ OSUSER_MINGID = int(get_env_variable('GVA_MIN_OS_GID'))
 OSUSER_USERNAME_PREFIX = get_env_variable('GVA_OSUSER_PREFIX')
 OSUSER_HOME_BASEPATH = get_env_variable('GVA_OSUSER_HOME_BASEPATH')
 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

From 0d08d9876b25a0b647c6934c1dca0ecb46c964ea Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sat, 24 Jan 2015 16:23:17 +0100
Subject: [PATCH 6/7] implement CustomerHostingPackageDetails view

---
 docs/code/hostingpackages.rst          | 14 +++++++++++
 gnuviechadmin/hostingpackages/urls.py  | 12 ++++++++-
 gnuviechadmin/hostingpackages/views.py | 34 ++++++++++++++++++++++----
 3 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/docs/code/hostingpackages.rst b/docs/code/hostingpackages.rst
index 2d5d077..ad021dc 100644
--- a/docs/code/hostingpackages.rst
+++ b/docs/code/hostingpackages.rst
@@ -22,3 +22,17 @@
 
 .. automodule:: hostingpackages.models
    :members:
+
+
+:py:mod:`views <hostingpackages.views>`
+---------------------------------------
+
+.. automodule:: hostingpackages.views
+   :members:
+
+
+:py:mod:`urls <hostingpackages.urls>`
+-------------------------------------
+
+.. automodule:: hostingpackages.urls
+   :members:
diff --git a/gnuviechadmin/hostingpackages/urls.py b/gnuviechadmin/hostingpackages/urls.py
index 9e7f00d..0280eca 100644
--- a/gnuviechadmin/hostingpackages/urls.py
+++ b/gnuviechadmin/hostingpackages/urls.py
@@ -1,12 +1,22 @@
+"""
+This module defines the URL patterns for hosting package related views.
+
+"""
 from __future__ import absolute_import, unicode_literals
 
 from django.conf.urls import patterns, url
 
-from .views import CreateHostingPackage
+from .views import (
+    CreateHostingPackage,
+    CustomerHostingPackageDetails,
+)
 
 
 urlpatterns = patterns(
     '',
     url(r'^(?P<user>[\w0-9@.+-_]+)/create$', CreateHostingPackage.as_view(),
         name='create_hosting_package'),
+    url(r'^(?P<user>[\w0-9@.+-_]+)/hostingpackage/(?P<pk>\d+)/$',
+        CustomerHostingPackageDetails.as_view(),
+        name='hosting_package_details'),
 )
diff --git a/gnuviechadmin/hostingpackages/views.py b/gnuviechadmin/hostingpackages/views.py
index a29c098..e368088 100644
--- a/gnuviechadmin/hostingpackages/views.py
+++ b/gnuviechadmin/hostingpackages/views.py
@@ -4,18 +4,22 @@ This module defines views related to hosting packages.
 """
 from __future__ import absolute_import, unicode_literals
 
+from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
+from django.views.generic import DetailView
 from django.views.generic.edit import CreateView
-from django.contrib.auth import get_user_model
 from django.contrib import messages
+from django.contrib.auth import get_user_model
 
 from braces.views import (
     LoginRequiredMixin,
     StaffuserRequiredMixin,
 )
 
+from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin
+
 from .forms import CreateHostingPackageForm
 from .models import CustomerHostingPackage
 
@@ -37,13 +41,12 @@ class CreateHostingPackage(
         kwargs.update(self.kwargs)
         return kwargs
 
-    def _get_customer(self):
+    def get_customer_object(self):
         return get_user_model().objects.get(username=self.kwargs['user'])
 
-
     def get_context_data(self, **kwargs):
         context = super(CreateHostingPackage, self).get_context_data(**kwargs)
-        context['customer'] = self._get_customer()
+        context['customer'] = self.get_customer_object()
         return context
 
     def get_success_url(self):
@@ -52,7 +55,7 @@ class CreateHostingPackage(
 
     def form_valid(self, form):
         hostingpackage = form.save(commit=False)
-        hostingpackage.customer = self._get_customer()
+        hostingpackage.customer = self.get_customer_object()
         hostingpackage.save()
         messages.success(
             self.request,
@@ -60,3 +63,24 @@ class CreateHostingPackage(
                 name=hostingpackage.name)
         )
         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

From 150366a524523eecc381f489ba192ed60aff0aed Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sat, 24 Jan 2015 16:26:32 +0100
Subject: [PATCH 7/7] plug users and hosting packages together

- document new feature in changelog
- add autogenerated documentation for osusers.urls and osusers.views
- add osuser URLs to gnuviechadmin.urls
- implement get_absolute_url in hostingpackages.models.CustomerHostingPackage
- use set_ldap_user_password instead of create_ldap_user for existing OS users
  in osusers.models.User.set_password
- add URL pattern set_osuser_password in osusers.urls
- implement osusers.views.SetOsUserPassword to set the password of an existing
  operating system user
- link to hosting package detail view on user dashboard
- add template hostingpackages/customerhostingpackage_detail.html
- add template osusers/user_setpassword.html
---
 docs/changelog.rst                            |  2 +
 docs/code/osusers.rst                         | 13 +++++
 gnuviechadmin/gnuviechadmin/urls.py           |  1 +
 gnuviechadmin/hostingpackages/models.py       |  7 +++
 gnuviechadmin/osusers/models.py               | 22 +++++++--
 gnuviechadmin/osusers/urls.py                 | 16 +++++++
 gnuviechadmin/osusers/views.py                | 45 +++++++++++++++++
 .../templates/dashboard/user_dashboard.html   |  2 +-
 .../customerhostingpackage_detail.html        | 48 +++++++++++++++++++
 gnuviechadmin/templates/osusers/base.html     |  1 +
 .../templates/osusers/user_setpassword.html   | 30 ++++++++++++
 11 files changed, 181 insertions(+), 6 deletions(-)
 create mode 100644 gnuviechadmin/osusers/urls.py
 create mode 100644 gnuviechadmin/osusers/views.py
 create mode 100644 gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html
 create mode 100644 gnuviechadmin/templates/osusers/base.html
 create mode 100644 gnuviechadmin/templates/osusers/user_setpassword.html

diff --git a/docs/changelog.rst b/docs/changelog.rst
index 99e4d05..d457f95 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,8 @@
 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
   URL in dashboard.urls
 * :feature:`-` add new task stub to set an ldap user's password
diff --git a/docs/code/osusers.rst b/docs/code/osusers.rst
index 79b7e05..3353239 100644
--- a/docs/code/osusers.rst
+++ b/docs/code/osusers.rst
@@ -30,3 +30,16 @@
 
 .. automodule:: osusers.models
    :members:
+
+
+:py:mod:`urls <osusers.urls>`
+-----------------------------
+
+.. automodule:: osusers.urls
+
+
+:py:mod:`views <osusers.views>`
+-------------------------------
+
+.. automodule:: osusers.views
+   :members:
diff --git a/gnuviechadmin/gnuviechadmin/urls.py b/gnuviechadmin/gnuviechadmin/urls.py
index 974ba1f..d3356b0 100644
--- a/gnuviechadmin/gnuviechadmin/urls.py
+++ b/gnuviechadmin/gnuviechadmin/urls.py
@@ -11,6 +11,7 @@ urlpatterns = patterns(
     url(r'', include('dashboard.urls')),
     url(r'^accounts/', include('allauth.urls')),
     url(r'^hosting/', include('hostingpackages.urls')),
+    url(r'^osuser/', include('osusers.urls')),
     url(r'^admin/', include(admin.site.urls)),
 )
 
diff --git a/gnuviechadmin/hostingpackages/models.py b/gnuviechadmin/hostingpackages/models.py
index b665162..021eb95 100644
--- a/gnuviechadmin/hostingpackages/models.py
+++ b/gnuviechadmin/hostingpackages/models.py
@@ -5,6 +5,7 @@ This module contains the hosting package models.
 from __future__ import absolute_import, unicode_literals
 
 from django.conf import settings
+from django.core.urlresolvers import reverse
 from django.db import transaction
 from django.db import models
 from django.utils.encoding import python_2_unicode_compatible
@@ -218,6 +219,12 @@ class CustomerHostingPackage(HostingPackageBase):
             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):
         """
         Copy the attributes of the hosting package's template to the package.
diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py
index c5c25a6..5f64a12 100644
--- a/gnuviechadmin/osusers/models.py
+++ b/gnuviechadmin/osusers/models.py
@@ -29,6 +29,7 @@ from ldaptasks.tasks import (
     delete_ldap_group,
     delete_ldap_user,
     remove_ldap_user_from_group,
+    set_ldap_user_password,
 )
 
 from fileservertasks.tasks import (
@@ -245,15 +246,26 @@ class User(TimeStampedModel, models.Model):
         """
         if hasattr(self, 'shadow'):
             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:
             self.shadow = Shadow.objects.create_shadow(
                 user=self, password=password
             )
-        dn = create_ldap_user.delay(
-            self.username, self.uid, self.group.gid, self.gecos,
-            self.homedir, self.shell, password
-        ).get()
-        logging.info("set LDAP password for %s", dn)
+            dn = create_ldap_user.delay(
+                self.username, self.uid, self.group.gid, self.gecos,
+                self.homedir, self.shell, password
+            ).get()
+            _LOGGER.info("set LDAP password for %s", dn)
+            return True
+
 
     @transaction.atomic
     def save(self, *args, **kwargs):
diff --git a/gnuviechadmin/osusers/urls.py b/gnuviechadmin/osusers/urls.py
new file mode 100644
index 0000000..7b62056
--- /dev/null
+++ b/gnuviechadmin/osusers/urls.py
@@ -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'),
+)
diff --git a/gnuviechadmin/osusers/views.py b/gnuviechadmin/osusers/views.py
new file mode 100644
index 0000000..a78067a
--- /dev/null
+++ b/gnuviechadmin/osusers/views.py
@@ -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)
diff --git a/gnuviechadmin/templates/dashboard/user_dashboard.html b/gnuviechadmin/templates/dashboard/user_dashboard.html
index fc2509a..929a088 100644
--- a/gnuviechadmin/templates/dashboard/user_dashboard.html
+++ b/gnuviechadmin/templates/dashboard/user_dashboard.html
@@ -22,7 +22,7 @@
           <tbody>
             {% for package in hosting_packages %}
             <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>
                 {% with diskspace=package.get_disk_space %}
                 <span title="{% blocktrans %}The reserved disk space for your hosting package is {{ diskspace }} bytes.{% endblocktrans %}">{{ diskspace|filesizeformat }}</span>
diff --git a/gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html b/gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html
new file mode 100644
index 0000000..daa06b5
--- /dev/null
+++ b/gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html
@@ -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 %}
diff --git a/gnuviechadmin/templates/osusers/base.html b/gnuviechadmin/templates/osusers/base.html
new file mode 100644
index 0000000..94d9808
--- /dev/null
+++ b/gnuviechadmin/templates/osusers/base.html
@@ -0,0 +1 @@
+{% extends "base.html" %}
diff --git a/gnuviechadmin/templates/osusers/user_setpassword.html b/gnuviechadmin/templates/osusers/user_setpassword.html
new file mode 100644
index 0000000..86fa844
--- /dev/null
+++ b/gnuviechadmin/templates/osusers/user_setpassword.html
@@ -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 %}