Added code for modules to register for first_boot #529

This commit is contained in:
Hemanth Kumar Veeranki 2016-10-30 18:07:39 +05:30 committed by James Valleroy
parent 3fa243ef5b
commit 0332d4489e
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
14 changed files with 297 additions and 78 deletions

View File

@ -22,3 +22,12 @@ Plinth module for first boot wizard
version = 1
is_essential = True
first_boot_steps = [{'id': 'firstboot_state0',
'url': 'first_boot:state0',
'order': 0
},
{'id': 'firstboot_state10',
'url': 'first_boot:state10',
'order': 10
}
]

View File

@ -23,9 +23,9 @@ yet.
from django.http.response import HttpResponseRedirect
from django.urls import reverse
import logging
from plinth import kvstore
from operator import itemgetter
from plinth import kvstore, module_loader
from django.shortcuts import render
LOGGER = logging.getLogger(__name__)
@ -37,19 +37,50 @@ class FirstBootMiddleware(object):
def process_request(request):
"""Handle a request as Django middleware request handler."""
state = kvstore.get_default('firstboot_state', 0)
firstboot_index_url = reverse('first_boot:index')
user_requests_firstboot = request.path.startswith(firstboot_index_url)
help_index_url = reverse('help:index')
user_requests_help = request.path.startswith(help_index_url)
# Setup is complete: Forbid accessing firstboot
if state >= 10 and user_requests_firstboot:
user_requests_firstboot = is_firstboot(request.path)
if state == 1 and user_requests_firstboot:
return HttpResponseRedirect(reverse('index'))
elif state == 0 and not user_requests_firstboot:
url = next_step()
return HttpResponseRedirect(reverse(url))
# Setup is not complete: Forbid accessing anything but
# firstboot or help
if state < 10 and not user_requests_firstboot and \
not user_requests_help:
return HttpResponseRedirect(reverse('first_boot:state%d' % state))
def is_firstboot(path):
"""
Returns whether the path is a firstboot step url
:param path: path of current url
:return: true if its a first boot url false otherwise
"""
steps = get_firstboot_steps()
for step in steps:
if reverse(step.get('url')) == path:
return True
return False
def get_firstboot_steps():
steps = []
modules = module_loader.loaded_modules
for (module_name, module_object) in modules.items():
if getattr(module_object, 'first_boot_steps', None):
for step in module_object.first_boot_steps:
steps.append(step)
steps = sorted(steps, key=itemgetter('order'))
return steps
def next_step():
""" Returns the next first boot step required to run """
steps = get_firstboot_steps()
for step in steps:
done = kvstore.get_default(step.get('id'), 0)
if done == 0:
return step.get('url')
def mark_step_done(id):
"""
Marks the status of a first boot step is done
:param id: id of the firstboot step
"""
kvstore.set(id, 1)

View File

@ -22,14 +22,12 @@ URLs for the First Boot module
from django.conf.urls import url
from stronghold.decorators import public
from .views import State0View, State1View, State5View, state10
from .views import State0View, state10
urlpatterns = [
# Take care of the firstboot middleware when changing URLs
url(r'^firstboot/$', public(State0View.as_view()), name='index'),
url(r'^firstboot/state0/$', public(State0View.as_view()), name='state0'),
url(r'^firstboot/state1/$', public(State1View.as_view()), name='state1'),
url(r'^firstboot/state5/$', State5View.as_view(), name='state5'),
url(r'^firstboot/state10/$', state10, name='state10'),
]

View File

@ -28,33 +28,15 @@ from plinth import kvstore
from plinth import network
from plinth.errors import DomainRegistrationError
from .forms import State1Form, State5Form
from .middleware import mark_step_done
class State0View(TemplateView):
"""Show the welcome screen."""
kvstore.set('firstboot_state0', 'done')
template_name = 'firstboot_state0.html'
class State1View(CreateView):
"""Create user account and log the user in."""
template_name = 'firstboot_state1.html'
form_class = State1Form
success_url = reverse_lazy('first_boot:state10')
def __init__(self, *args, **kwargs):
"""Initialize the view object."""
if cfg.danube_edition:
self.success_url = reverse_lazy('first_boot:state5')
return super(State1View, self).__init__(*args, **kwargs)
def get_form_kwargs(self):
"""Make request available to the form (to insert messages)"""
kwargs = super(State1View, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def state10(request):
"""State 10 is when all firstboot setup is done.
@ -62,36 +44,10 @@ def state10(request):
"""
# Make sure that a user exists before finishing firstboot
if User.objects.all():
kvstore.set('firstboot_state', 10)
mark_step_done('firstboot_state')
connections = network.get_connection_list()
return render(request, 'firstboot_state10.html',
{'title': _('Setup Complete'),
'connections': connections})
class State5View(FormView):
"""State 5 is the (optional) setup of the Pagekite subdomain."""
template_name = 'firstboot_state5.html'
form_class = State5Form
success_url = reverse_lazy('first_boot:state10')
def get(self, *args, **kwargs):
"""Respond to GET request."""
kvstore.set('firstboot_state', 5)
return super(State5View, self).get(*args, **kwargs)
def form_valid(self, form):
"""Act on valid form submission."""
try:
form.register_domain()
except DomainRegistrationError as error:
messages.error(self.request, error)
return HttpResponseRedirect(reverse_lazy('first_boot:state5'))
else:
form.setup_pagekite()
message = _('Pagekite setup finished. The HTTP and HTTPS services '
'are activated now.')
messages.success(self.request, message)
return super(State5View, self).form_valid(form)

View File

@ -31,6 +31,12 @@ depends = ['system', 'names']
managed_packages = ['pagekite']
first_boot_steps = [{'id': 'pagekite_firstboot',
'url': 'pagekite:firstboot',
'order': 5,
},
]
title = _('Public Visibility (PageKite)')
description = [

View File

@ -18,12 +18,19 @@
import copy
from django import forms
from django.contrib import messages
from django.contrib.sites import requests
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _, ugettext_lazy
import json
import logging
from plinth.errors import ActionError
from plinth import cfg
from plinth.errors import ActionError, DomainRegistrationError
from plinth.modules.first_boot.forms import SubdomainWidget
from plinth.modules.pagekite.utils import PREDEFINED_SERVICES, run
from plinth.utils import format_lazy
from . import utils
LOGGER = logging.getLogger(__name__)
@ -237,3 +244,94 @@ class AddCustomServiceForm(BaseCustomServiceForm):
messages.error(request, _('This service already exists'))
else:
raise
class State5Form(forms.Form):
"""Set up freedombox.me pagekite subdomain"""
DOMAIN_APPENDIX = '.freedombox.me'
# Webservice url for domain validation and registration
service_url = 'http://freedombox.me/cgi-bin/freedomkite.pl'
code_help_text = format_lazy(
ugettext_lazy('The voucher you received with your {box_name} Danube '
'Edition'), box_name=ugettext_lazy(cfg.box_name))
code = forms.CharField(help_text=code_help_text)
domain = forms.SlugField(label=_('Subdomain'),
widget=SubdomainWidget(domain=DOMAIN_APPENDIX),
help_text=_('The subdomain you want to register'))
def clean_domain(self):
"""Append the domain to the users' subdomain"""
return self.cleaned_data['domain'] + self.DOMAIN_APPENDIX
def clean(self):
"""Validate user input (subdomain and code)"""
cleaned_data = super().clean()
# If the subdomain is wrong, don't look if the domain is
# available
if self.errors:
return cleaned_data
self.domain_already_registered = False
code = cleaned_data.get('code')
domain = cleaned_data.get('domain')
response = requests.get(self.service_url, params={'code': code}).json()
# 1. Code is invalid: {}
if 'domain' not in response:
raise ValidationError(_('This code is not valid'), code='invalid')
# 2. Code is valid, domain registered: {'domain': 'xx.freedombox.me'}
elif response['domain']:
if response['domain'] == domain:
self.domain_already_registered = True
else:
message = _('This code is bound to the domain {domain}.') \
.format(domain=response['domain'])
raise ValidationError(message, code='invalid')
# 3. Code is valid, no domain registered: {'domain': None}
elif response['domain'] is None:
# Make sure that the desired domain is available
data = {'domain': domain}
domain_response = requests.get(self.service_url, params=data)
registered_domain = domain_response.json()['domain']
if registered_domain is not None:
message = _('The requested domain is already registered.')
raise ValidationError(message, code='invalid')
return cleaned_data
def register_domain(self):
"""Register a domain (only if it's not already registered)"""
if self.domain_already_registered:
return
data = {'domain': self.cleaned_data['domain'],
'code': self.cleaned_data['code']}
response = requests.post(self.service_url, data)
if not response.ok:
message = _('Domain registration failed: {response}.').format(
response=response.text)
LOGGER.error(message)
raise DomainRegistrationError(message)
def setup_pagekite(self):
"""Configure and enable PageKite service."""
# Set kite name and secret
run(['set-kite', '--kite-name', self.cleaned_data['domain']],
input=self.cleaned_data['code'].encode())
# Set frontend
run(['set-frontend', '%s:80' % self.cleaned_data['domain']])
# Enable PageKite HTTP + HTTPS service
for service_name in ['http', 'https']:
service = PREDEFINED_SERVICES[service_name]['params']
try:
run(['add-service', '--service', json.dumps(service)])
except ActionError as err:
if 'already exists' not in str(err):
raise
run(['start-and-enable'])

View File

@ -22,8 +22,7 @@ URLs for the PageKite module
from django.conf.urls import url
from .views import StandardServiceView, CustomServiceView, ConfigurationView, \
DeleteServiceView, index
DeleteServiceView, index, State5View
urlpatterns = [
url(r'^sys/pagekite/$', index, name='index'),
@ -35,4 +34,5 @@ urlpatterns = [
name='custom-services'),
url(r'^sys/pagekite/services/custom/delete$', DeleteServiceView.as_view(),
name='delete-custom-service'),
url(r'^sys/pagekite/firstboot/$', State5View.as_view(), name='firstboot'),
]

View File

@ -14,19 +14,21 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from django.contrib import messages
from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View, TemplateView
from django.views.generic.edit import FormView
from plinth import kvstore
from plinth.errors import DomainRegistrationError
from . import utils
from .forms import ConfigurationForm, StandardServiceForm, \
AddCustomServiceForm, DeleteCustomServiceForm
AddCustomServiceForm, DeleteCustomServiceForm, State5Form
from plinth.modules import pagekite
from plinth.modules.first_boot.middleware import mark_step_done
subsubmenu = [{'url': reverse_lazy('pagekite:index'),
'text': _('About PageKite')},
@ -51,6 +53,7 @@ class ContextMixin(object):
Also adds the requirement of all necessary packages to be installed
"""
def get_context_data(self, **kwargs):
"""Use self.title and the module-level subsubmenu"""
context = super(ContextMixin, self).get_context_data(**kwargs)
@ -129,3 +132,28 @@ class ConfigurationView(ContextMixin, FormView):
def form_valid(self, form):
form.save(self.request)
return super(ConfigurationView, self).form_valid(form)
class State5View(FormView):
"""State 5 is the (optional) setup of the Pagekite subdomain."""
template_name = 'firstboot_state5.html'
form_class = State5Form
def get(self, *args, **kwargs):
"""Respond to GET request."""
mark_step_done('pagekite_firstboot')
return super(State5View, self).get(*args, **kwargs)
def form_valid(self, form):
"""Act on valid form submission."""
try:
form.register_domain()
except DomainRegistrationError as error:
messages.error(self.request, error)
return HttpResponseRedirect(reverse_lazy('pagekite:firstboot'))
else:
form.setup_pagekite()
message = _('Pagekite setup finished. The HTTP and HTTPS services '
'are activated now.')
messages.success(self.request, message)
return super(State5View, self).form_valid(form)

View File

@ -34,7 +34,11 @@ depends = ['system']
managed_packages = ['ldapscripts', 'ldap-utils', 'libnss-ldapd',
'libpam-ldapd', 'nslcd', 'slapd']
first_boot_steps = [{'id': 'users_firstboot',
'url': 'users:firstboot',
'order': 1
},
]
title = _('Users and Groups')
@ -76,4 +80,4 @@ def _diagnose_ldap_entry(search_item):
pass
return [_('Check LDAP entry "{search_item}"')
.format(search_item=search_item), result]
.format(search_item=search_item), result]

View File

@ -18,6 +18,7 @@
import subprocess
from django import forms
from django.contrib import auth
from django.contrib import messages
from django.contrib.auth.models import User, Group
from django.contrib.auth.forms import UserCreationForm, SetPasswordForm
@ -28,6 +29,8 @@ from plinth import actions
from plinth.errors import ActionError
# Usernames used by optional services (that might not be installed yet).
from plinth.modules.security import set_restricted_access
RESERVED_USERNAMES = [
'debian-deluged',
'Debian-minetest',
@ -240,3 +243,64 @@ class UserChangePasswordForm(SetPasswordForm):
_('Changing LDAP user password failed.'))
return user
class State1Form(ValidNewUsernameCheckMixin, auth.forms.UserCreationForm):
"""Firstboot state 1: create a new user."""
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
def save(self, commit=True):
"""Create and log the user in."""
user = super().save(commit=commit)
if commit:
try:
actions.superuser_run(
'ldap',
['create-user', user.get_username()],
input=self.cleaned_data['password1'].encode())
except ActionError:
messages.error(self.request,
_('Creating LDAP user failed.'))
try:
actions.superuser_run(
'ldap',
['add-user-to-group', user.get_username(), 'admin'])
except ActionError:
messages.error(self.request,
_('Failed to add new user to admin group.'))
# Create initial Django groups
for group_choice in GROUP_CHOICES:
auth.models.Group.objects.get_or_create(name=group_choice[0])
admin_group = auth.models.Group.objects.get(name='admin')
admin_group.user_set.add(user)
self.login_user(self.cleaned_data['username'],
self.cleaned_data['password1'])
# Restrict console login to users in admin or sudo group
try:
set_restricted_access(True)
message = _('Console login access restricted to users in '
'"admin" group. This can be configured in '
'security settings.')
messages.success(self.request, message)
except Exception:
messages.error(self.request,
_('Failed to restrict console access.'))
return user
def login_user(self, username, password):
"""Try to login the user with the credentials provided"""
try:
user = auth.authenticate(username=username, password=password)
auth.login(self.request, user)
except Exception:
pass
else:
message = _('User account created, you are now logged in')
messages.success(self.request, message)

View File

@ -22,6 +22,7 @@ URLs for the Users module
from django.conf.urls import url
from django.contrib.auth import views as auth_views
from django.urls import reverse_lazy
from stronghold.decorators import public
from . import views
@ -40,4 +41,5 @@ urlpatterns = [
{'template_name': 'login.html'}, name='login'),
url(r'^accounts/logout/$', auth_views.logout,
{'next_page': reverse_lazy('index')}, name='logout'),
url(r'^users/firstboot/$', public(views.State1View.as_view()), name='firstboot'),
]

View File

@ -22,13 +22,16 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse, reverse_lazy
from django.views.generic.edit import (CreateView, DeleteView, UpdateView,
FormView)
from django.views.generic import ListView
from django.views.generic import ListView, CreateView
from django.utils.translation import ugettext as _, ugettext_lazy
from plinth import cfg
from plinth import kvstore
from .forms import CreateUserForm, UserChangePasswordForm, UserUpdateForm, State1Form
from .forms import CreateUserForm, UserChangePasswordForm, UserUpdateForm
from plinth import actions
from plinth.errors import ActionError
from plinth.modules.first_boot.middleware import mark_step_done
subsubmenu = [{'url': reverse_lazy('users:index'),
'text': ugettext_lazy('Users')},
@ -38,6 +41,7 @@ subsubmenu = [{'url': reverse_lazy('users:index'),
class ContextMixin(object):
"""Mixin to add 'subsubmenu' and 'title' to the context."""
def get_context_data(self, **kwargs):
"""Use self.title and the module-level subsubmenu"""
context = super(ContextMixin, self).get_context_data(**kwargs)
@ -163,3 +167,22 @@ class UserChangePassword(ContextMixin, SuccessMessageMixin, FormView):
form.save()
update_session_auth_hash(self.request, form.user)
return super(UserChangePassword, self).form_valid(form)
class State1View(CreateView):
"""Create user account and log the user in."""
template_name = 'firstboot_state1.html'
form_class = State1Form
def __init__(self, *args, **kwargs):
"""Initialize the view object."""
if not cfg.danube_edition:
mark_step_done('pagekite_firstboot')
mark_step_done('users_firstboot')
return super(State1View, self).__init__(*args, **kwargs)
def get_form_kwargs(self):
"""Make request available to the form (to insert messages)"""
kwargs = super(State1View, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs