Browse Source

Added auth support with PAM

Óscar García Amor 3 years ago
parent
commit
17c5972229
4 changed files with 66 additions and 4 deletions
  1. 1 0
      requirements.txt
  2. 9 2
      sysdweb.conf
  3. 36 0
      sysdweb/config.py
  4. 20 2
      sysdweb/server.py

+ 1 - 0
requirements.txt

@@ -1,3 +1,4 @@
 bottle >= 0.12.10
 dbus-python
+python-pam
 systemd-python

+ 9 - 2
sysdweb.conf

@@ -25,11 +25,18 @@
 scope = system
 
 # Can configure listen address host and port. If not present, default values
-# will be used. Take note that you can pass this values via args and they
-# prevail over this configuration.
+# will be used. Take note that you can pass this values via environment vars
+# or args and they prevail over this configuration.
 #host = 127.0.0.1
 #port = 10080
 
+# sysdweb uses PAM to users auth, if you are running it in system mode you
+# can define here what users and groups have access. If you leave this
+# unconfigured them all system users will have access. In user mode this not
+# apply because only the user that running it have access.
+#users = root, admin
+#groups = wheel, users
+
 # Some sample entries
 [ngx]
 title = Nginx

+ 36 - 0
sysdweb/config.py

@@ -7,8 +7,10 @@
 # Distributed under terms of the GNU GPLv3 license.
 
 import configparser
+import grp
 import logging
 import os
+import pwd
 
 def checkConfig(file=None):
     """
@@ -37,6 +39,40 @@ def checkConfig(file=None):
         err = 'sysdweb config file is corrupted.\n{0}'.format(e)
         raise SystemExit(err)
 
+    # Configure valid users
+    if config.get('DEFAULT', 'scope', fallback='system') == 'system':
+        # Get a full list of users
+        users = config.get('DEFAULT', 'users', fallback=None)
+        groups = config.get('DEFAULT', 'groups', fallback=None)
+
+        if users:
+            users = [user.strip() for user in users.split(',')]
+        else:
+            users = []
+
+        if groups:
+            groups = [group.strip() for group in groups.split(',')]
+            # Obtain usenames from groups
+            for group in groups:
+                try:
+                    users.extend(grp.getgrnam(group)[3])
+                except KeyError:
+                    logger.warning('Group \'{}\' not found in database, skipped.'.format(group))
+
+        # If left any user in list send to main process
+        if users:
+            users = list(set(users)) # Remove duplicates
+            users.sort() # Sort alphabetically
+            logger.debug('Running in system mode, valid users \'{}\'.'.format(', '.join(users)))
+            config.set('DEFAULT', 'users', ','.join(users))
+        else:
+            logger.debug('Running in system mode, ALL system users are valid.')
+    else:
+        # Only current user can log in
+        user = pwd.getpwuid(os.getuid())[0]
+        logger.debug('Running in user mode, valid user \'{}\'.'.format(user))
+        config.set('DEFAULT', 'users', user)
+
     # Read all sections to check if are correctly configurated
     for section in config.sections():
         if not config.get(section, 'title', fallback=None):

+ 20 - 2
sysdweb/server.py

@@ -6,12 +6,12 @@
 #
 # Distributed under terms of the GNU GPLv3 license.
 
-from bottle import abort, response, route, run, static_file, template, TEMPLATE_PATH
+from bottle import abort, auth_basic, response, route, run, static_file, template, TEMPLATE_PATH
+from pam import pam
 from socket import gethostname
 from sysdweb.config import checkConfig
 from sysdweb.systemd import systemdBus, Journal
 
-import json
 import os
 
 # Search for template path
@@ -23,7 +23,17 @@ if template_path == []:
 TEMPLATE_PATH.insert(0, os.path.join(template_path[0], 'views'))
 static_path = os.path.join(template_path[0], 'static')
 
+# Define auth function
+def login(user, password):
+    users = config.get('DEFAULT', 'users', fallback=None)
+    if users and not user in users.split(','):
+        # User not is in the valid user list
+        return False
+    # Validate user with password
+    return pam().authenticate(user, password)
+
 @route('/api/v1/<service>/<action>')
+@auth_basic(login)
 def get_service_action(service, action):
     if service in config.sections():
         sdbus = systemdBus(True) if config.get('DEFAULT', 'scope', fallback='system') == 'user' else systemdBus()
@@ -53,6 +63,7 @@ def get_service_action(service, action):
         return {'msg': 'Sorry, but \'{}\' is not defined in config.'.format(service)}
 
 @route('/api/v1/<service>/journal/<lines>')
+@auth_basic(login)
 def get_service_journal(service, lines):
     if service in config.sections():
         if get_service_action(service, 'status')['status'] == 'not-found':
@@ -70,6 +81,7 @@ def get_service_journal(service, lines):
         return {'msg': 'Sorry, but \'{}\' is not defined in config.'.format(service)}
 
 @route('/')
+@auth_basic(login)
 def get_main():
     services = []
     for service in config.sections():
@@ -94,6 +106,7 @@ def get_main():
     return template('index', hostname=gethostname(), services=services)
 
 @route('/journal/<service>')
+@auth_basic(login)
 def get_service_journal_page(service):
     if service in config.sections():
         if get_service_action(service, 'status')['status'] == 'not-found':
@@ -105,22 +118,27 @@ def get_service_journal_page(service):
 
 # Serve static content
 @route('/favicon.ico')
+@auth_basic(login)
 def get_favicon():
     return static_file('favicon.ico', root=os.path.join(static_path, 'img'))
 
 @route('/css/<file>')
+@auth_basic(login)
 def get_css(file):
     return static_file(file, root=os.path.join(static_path, 'css'))
 
 @route('/fonts/<file>')
+@auth_basic(login)
 def get_fonts(file):
     return static_file(file, root=os.path.join(static_path, 'fonts'))
 
 @route('/img/<file>')
+@auth_basic(login)
 def get_img(file):
     return static_file(file, root=os.path.join(static_path, 'img'))
 
 @route('/js/<file>')
+@auth_basic(login)
 def get_js(file):
     return static_file(file, root=os.path.join(static_path, 'js'))