From 8e96e828d940ed7dd503b1f0ed86807c58c527a5 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sun, 12 Jun 2016 14:48:46 -0400 Subject: [PATCH] security: New module to control login restrictions --- actions/security | 80 +++++++++++++++++ actions/users | 15 ---- data/etc/plinth/modules-enabled/security | 1 + plinth/modules/security/__init__.py | 39 ++++++++ plinth/modules/security/forms.py | 32 +++++++ .../modules/security/templates/security.html | 35 ++++++++ plinth/modules/security/tests/__init__.py | 0 plinth/modules/security/urls.py | 29 ++++++ plinth/modules/security/views.py | 90 +++++++++++++++++++ 9 files changed, 306 insertions(+), 15 deletions(-) create mode 100755 actions/security create mode 100644 data/etc/plinth/modules-enabled/security create mode 100644 plinth/modules/security/__init__.py create mode 100644 plinth/modules/security/forms.py create mode 100644 plinth/modules/security/templates/security.html create mode 100644 plinth/modules/security/tests/__init__.py create mode 100644 plinth/modules/security/urls.py create mode 100644 plinth/modules/security/views.py diff --git a/actions/security b/actions/security new file mode 100755 index 000000000..3567f9896 --- /dev/null +++ b/actions/security @@ -0,0 +1,80 @@ +#!/usr/bin/python3 +# -*- mode: python -*- +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Helper for security configuration +""" + +import argparse + + +ACCESS_CONF_FILE = '/etc/security/access.conf' +ACCESS_CONF_SNIPPET = '-:ALL EXCEPT root fbx (admin) (sudo):ALL' + + +def parse_arguments(): + """Return parsed command line arguments as dictionary""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + subparsers.add_parser( + 'enable-restricted-access', + help='Restrict console login to users in admin or sudo group') + subparsers.add_parser( + 'disable-restricted-access', + help='Don\'t restrict console login to users in admin or sudo group') + + return parser.parse_args() + + +def subcommand_enable_restricted_access(_): + """Restrict console login to users in admin or sudo group.""" + with open(ACCESS_CONF_FILE, 'r') as conffile: + lines = conffile.readlines() + + for line in lines: + if ACCESS_CONF_SNIPPET in line: + return + + with open(ACCESS_CONF_FILE, 'a') as conffile: + conffile.write(ACCESS_CONF_SNIPPET + '\n') + + +def subcommand_disable_restricted_access(_): + """Don't restrict console login to users in admin or sudo group.""" + with open(ACCESS_CONF_FILE, 'r') as conffile: + lines = conffile.readlines() + + with open(ACCESS_CONF_FILE, 'w') as conffile: + for line in lines: + if ACCESS_CONF_SNIPPET not in line: + conffile.write(line) + + +def main(): + """Parse arguments and perform all duties""" + arguments = parse_arguments() + + subcommand = arguments.subcommand.replace('-', '_') + subcommand_method = globals()['subcommand_' + subcommand] + subcommand_method(arguments) + + +if __name__ == '__main__': + main() diff --git a/actions/users b/actions/users index 093329059..90c424d65 100755 --- a/actions/users +++ b/actions/users @@ -75,8 +75,6 @@ def subcommand_pre_install(_): def subcommand_setup(_): """Setup LDAP.""" - configure_access_conf() - # Update pam configs for access and mkhomedir. subprocess.run(['pam-auth-update', '--package'], check=True) @@ -153,19 +151,6 @@ olcRootDN: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth ''') -def configure_access_conf(): - """Restrict console login to users in admin or sudo group.""" - with open(ACCESS_CONF, 'r') as conffile: - lines = conffile.readlines() - - for line in lines: - if '-:ALL EXCEPT root fbx (admin) (sudo):ALL' in line: - return - - with open(ACCESS_CONF, 'a') as conffile: - conffile.write('-:ALL EXCEPT root fbx (admin) (sudo):ALL\n') - - def configure_ldapscripts(): """Set the configuration used by ldapscripts for later user management.""" aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + diff --git a/data/etc/plinth/modules-enabled/security b/data/etc/plinth/modules-enabled/security new file mode 100644 index 000000000..7cd6eb1fb --- /dev/null +++ b/data/etc/plinth/modules-enabled/security @@ -0,0 +1 @@ +plinth.modules.security diff --git a/plinth/modules/security/__init__.py b/plinth/modules/security/__init__.py new file mode 100644 index 000000000..6eb292653 --- /dev/null +++ b/plinth/modules/security/__init__.py @@ -0,0 +1,39 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Plinth module for security configuration +""" + +from django.utils.translation import ugettext_lazy as _ + +from plinth import cfg + + +version = 1 + +is_essential = True + +depends = ['system'] + +title = _('Security') + + +def init(): + """Initialize the module""" + menu = cfg.main_menu.get('system:index') + menu.add_urlname(title, 'glyphicon-lock', 'security:index') diff --git a/plinth/modules/security/forms.py b/plinth/modules/security/forms.py new file mode 100644 index 000000000..44dbf49c8 --- /dev/null +++ b/plinth/modules/security/forms.py @@ -0,0 +1,32 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Forms for security module +""" + +from django import forms +from django.utils.translation import ugettext_lazy as _ + + +class SecurityForm(forms.Form): + """Security configuration form""" + restricted_access = forms.BooleanField( + label=_('Restrict console logins'), required=False, + help_text=_('When this option is enabled, only users in the "admin" ' + 'group will be able to login through an attached ' + 'keyboard/monitor or serial console.')) diff --git a/plinth/modules/security/templates/security.html b/plinth/modules/security/templates/security.html new file mode 100644 index 000000000..996f16704 --- /dev/null +++ b/plinth/modules/security/templates/security.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% comment %} +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} + +
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+ +{% endblock %} diff --git a/plinth/modules/security/tests/__init__.py b/plinth/modules/security/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/modules/security/urls.py b/plinth/modules/security/urls.py new file mode 100644 index 000000000..04f972db9 --- /dev/null +++ b/plinth/modules/security/urls.py @@ -0,0 +1,29 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +URLs for the security module +""" + +from django.conf.urls import url + +from . import views + + +urlpatterns = [ + url(r'^sys/security/$', views.index, name='index'), +] diff --git a/plinth/modules/security/views.py b/plinth/modules/security/views.py new file mode 100644 index 000000000..affc29b66 --- /dev/null +++ b/plinth/modules/security/views.py @@ -0,0 +1,90 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Views for security module +""" + +from django.contrib import messages +from django.template.response import TemplateResponse +from django.utils.translation import ugettext as _ + +from .forms import SecurityForm +from plinth import actions + + +ACCESS_CONF_FILE = '/etc/security/access.conf' +ACCESS_CONF_SNIPPET = '-:ALL EXCEPT root fbx (admin) (sudo):ALL' + + +def index(request): + """Serve the security configuration form""" + status = get_status(request) + + form = None + + if request.method == 'POST': + form = SecurityForm(request.POST, initial=status, prefix='security') + if form.is_valid(): + _apply_changes(request, status, form.cleaned_data) + status = get_status(request) + form = SecurityForm(initial=status, prefix='security') + else: + form = SecurityForm(initial=status, prefix='security') + + return TemplateResponse(request, 'security.html', + {'title': _('Security'), + 'form': form}) + + +def get_status(request): + """Return the current status""" + return {'restricted_access': get_restricted_access_enabled()} + + +def _apply_changes(request, old_status, new_status): + """Apply the form changes""" + if old_status['restricted_access'] != new_status['restricted_access']: + try: + set_restricted_access(new_status['restricted_access']) + except Exception as exception: + messages.error( + request, + _('Error setting restricted access: {exception}') + .format(exception=exception)) + else: + messages.success(request, _('Updated security configuration')) + + +def get_restricted_access_enabled(): + """Return whether restricted access is enabled""" + with open(ACCESS_CONF_FILE, 'r') as conffile: + lines = conffile.readlines() + + for line in lines: + if ACCESS_CONF_SNIPPET in line: + return True + + return False + + +def set_restricted_access(enabled): + """Enable or disable restricted access""" + action = 'disable-restricted-access' + if enabled: + action = 'enable-restricted-access' + actions.superuser_run('security', [action])