mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-08-19 12:36:06 +00:00
Added code for modules to register for first_boot #529
This commit is contained in:
parent
3fa243ef5b
commit
0332d4489e
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user