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 %}
+
+
+
+{% 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])