summaryrefslogtreecommitdiffstats
path: root/deluge/core/authmanager.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/core/authmanager.py')
-rw-r--r--deluge/core/authmanager.py285
1 files changed, 285 insertions, 0 deletions
diff --git a/deluge/core/authmanager.py b/deluge/core/authmanager.py
new file mode 100644
index 0000000..3ff8a3a
--- /dev/null
+++ b/deluge/core/authmanager.py
@@ -0,0 +1,285 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+import os
+import shutil
+
+import deluge.component as component
+import deluge.configmanager as configmanager
+from deluge.common import (
+ AUTH_LEVEL_ADMIN,
+ AUTH_LEVEL_DEFAULT,
+ AUTH_LEVEL_NONE,
+ AUTH_LEVEL_NORMAL,
+ AUTH_LEVEL_READONLY,
+ create_localclient_account,
+)
+from deluge.error import AuthenticationRequired, AuthManagerError, BadLoginError
+
+log = logging.getLogger(__name__)
+
+AUTH_LEVELS_MAPPING = {
+ 'NONE': AUTH_LEVEL_NONE,
+ 'READONLY': AUTH_LEVEL_READONLY,
+ 'DEFAULT': AUTH_LEVEL_DEFAULT,
+ 'NORMAL': AUTH_LEVEL_NORMAL,
+ 'ADMIN': AUTH_LEVEL_ADMIN,
+}
+AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
+
+
+class Account:
+ __slots__ = ('username', 'password', 'authlevel')
+
+ def __init__(self, username, password, authlevel):
+ self.username = username
+ self.password = password
+ self.authlevel = authlevel
+
+ def data(self):
+ return {
+ 'username': self.username,
+ 'password': self.password,
+ 'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel],
+ 'authlevel_int': self.authlevel,
+ }
+
+ def __repr__(self):
+ return '<Account username="{username}" authlevel={authlevel}>'.format(
+ username=self.username,
+ authlevel=self.authlevel,
+ )
+
+
+class AuthManager(component.Component):
+ def __init__(self):
+ component.Component.__init__(self, 'AuthManager', interval=10)
+ self.__auth = {}
+ self.__auth_modification_time = None
+
+ def start(self):
+ self.__load_auth_file()
+
+ def stop(self):
+ self.__auth = {}
+
+ def shutdown(self):
+ pass
+
+ def update(self):
+ auth_file = configmanager.get_config_dir('auth')
+ # Check for auth file and create if necessary
+ if not os.path.isfile(auth_file):
+ log.info('Authfile not found, recreating it.')
+ self.__load_auth_file()
+ return
+
+ auth_file_modification_time = os.stat(auth_file).st_mtime
+ if self.__auth_modification_time != auth_file_modification_time:
+ log.info('Auth file changed, reloading it!')
+ self.__load_auth_file()
+
+ def authorize(self, username, password):
+ """Authorizes users based on username and password.
+
+ Args:
+ username (str): Username
+ password (str): Password
+
+ Returns:
+ int: The auth level for this user.
+
+ Raises:
+ AuthenticationRequired: If additional details are required to authenticate.
+ BadLoginError: If the username does not exist or password does not match.
+
+ """
+ if not username:
+ raise AuthenticationRequired(
+ 'Username and Password are required.', username
+ )
+
+ if username not in self.__auth:
+ # Let's try to re-load the file.. Maybe it's been updated
+ self.__load_auth_file()
+ if username not in self.__auth:
+ raise BadLoginError('Username does not exist', username)
+
+ if self.__auth[username].password == password:
+ # Return the users auth level
+ return self.__auth[username].authlevel
+ elif not password and self.__auth[username].password:
+ raise AuthenticationRequired('Password is required', username)
+ else:
+ raise BadLoginError('Password does not match', username)
+
+ def has_account(self, username):
+ return username in self.__auth
+
+ def get_known_accounts(self):
+ """Returns a list of known deluge usernames."""
+ self.__load_auth_file()
+ return [account.data() for account in self.__auth.values()]
+
+ def create_account(self, username, password, authlevel):
+ if username in self.__auth:
+ raise AuthManagerError('Username in use.', username)
+ if authlevel not in AUTH_LEVELS_MAPPING:
+ raise AuthManagerError('Invalid auth level: %s' % authlevel)
+ try:
+ self.__auth[username] = Account(
+ username, password, AUTH_LEVELS_MAPPING[authlevel]
+ )
+ self.write_auth_file()
+ return True
+ except Exception as ex:
+ log.exception(ex)
+ raise ex
+
+ def update_account(self, username, password, authlevel):
+ if username not in self.__auth:
+ raise AuthManagerError('Username not known', username)
+ if authlevel not in AUTH_LEVELS_MAPPING:
+ raise AuthManagerError('Invalid auth level: %s' % authlevel)
+ try:
+ self.__auth[username].username = username
+ self.__auth[username].password = password
+ self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel]
+ self.write_auth_file()
+ return True
+ except Exception as ex:
+ log.exception(ex)
+ raise ex
+
+ def remove_account(self, username):
+ if username not in self.__auth:
+ raise AuthManagerError('Username not known', username)
+ elif username == component.get('RPCServer').get_session_user():
+ raise AuthManagerError(
+ 'You cannot delete your own account while logged in!', username
+ )
+
+ del self.__auth[username]
+ self.write_auth_file()
+ return True
+
+ def write_auth_file(self):
+ filename = 'auth'
+ filepath = os.path.join(configmanager.get_config_dir(), filename)
+ filepath_bak = filepath + '.bak'
+ filepath_tmp = filepath + '.tmp'
+
+ try:
+ if os.path.isfile(filepath):
+ log.debug('Creating backup of %s at: %s', filename, filepath_bak)
+ shutil.copy2(filepath, filepath_bak)
+ except OSError as ex:
+ log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
+ else:
+ log.info('Saving the %s at: %s', filename, filepath)
+ try:
+ with open(filepath_tmp, 'w', encoding='utf8') as _file:
+ for account in self.__auth.values():
+ _file.write(
+ '%(username)s:%(password)s:%(authlevel_int)s\n'
+ % account.data()
+ )
+ _file.flush()
+ os.fsync(_file.fileno())
+ shutil.move(filepath_tmp, filepath)
+ except OSError as ex:
+ log.error('Unable to save %s: %s', filename, ex)
+ if os.path.isfile(filepath_bak):
+ log.info('Restoring backup of %s from: %s', filename, filepath_bak)
+ shutil.move(filepath_bak, filepath)
+
+ self.__load_auth_file()
+
+ def __load_auth_file(self):
+ save_and_reload = False
+ filename = 'auth'
+ auth_file = configmanager.get_config_dir(filename)
+ auth_file_bak = auth_file + '.bak'
+
+ # Check for auth file and create if necessary
+ if not os.path.isfile(auth_file):
+ create_localclient_account()
+ return self.__load_auth_file()
+
+ auth_file_modification_time = os.stat(auth_file).st_mtime
+ if self.__auth_modification_time is None:
+ self.__auth_modification_time = auth_file_modification_time
+ elif self.__auth_modification_time == auth_file_modification_time:
+ # File didn't change, no need for re-parsing's
+ return
+
+ for _filepath in (auth_file, auth_file_bak):
+ log.info('Opening %s for load: %s', filename, _filepath)
+ try:
+ with open(_filepath, encoding='utf8') as _file:
+ file_data = _file.readlines()
+ except OSError as ex:
+ log.warning('Unable to load %s: %s', _filepath, ex)
+ file_data = []
+ else:
+ log.info('Successfully loaded %s: %s', filename, _filepath)
+ break
+
+ # Load the auth file into a dictionary: {username: Account(...)}
+ for line in file_data:
+ line = line.strip()
+ if line.startswith('#') or not line:
+ # This line is a comment or empty
+ continue
+ lsplit = line.split(':')
+ if len(lsplit) == 2:
+ username, password = lsplit
+ log.warning(
+ 'Your auth entry for %s contains no auth level, '
+ 'using AUTH_LEVEL_DEFAULT(%s)..',
+ username,
+ AUTH_LEVEL_DEFAULT,
+ )
+ if username == 'localclient':
+ authlevel = AUTH_LEVEL_ADMIN
+ else:
+ authlevel = AUTH_LEVEL_DEFAULT
+ # This is probably an old auth file
+ save_and_reload = True
+ elif len(lsplit) == 3:
+ username, password, authlevel = lsplit
+ else:
+ log.error('Your auth file is malformed: Incorrect number of fields!')
+ continue
+
+ username = username.strip()
+ password = password.strip()
+ try:
+ authlevel = int(authlevel)
+ except ValueError:
+ try:
+ authlevel = AUTH_LEVELS_MAPPING[authlevel]
+ except KeyError:
+ log.error(
+ 'Your auth file is malformed: %r is not a valid auth level',
+ authlevel,
+ )
+ continue
+
+ self.__auth[username] = Account(username, password, authlevel)
+
+ if 'localclient' not in self.__auth:
+ create_localclient_account(True)
+ return self.__load_auth_file()
+
+ if save_and_reload:
+ log.info('Re-writing auth file (upgrade)')
+ self.write_auth_file()
+ self.__auth_modification_time = auth_file_modification_time