320 lines
11 KiB
Python
320 lines
11 KiB
Python
#
|
|
# authmanager.py
|
|
#
|
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
|
# Copyright (C) 2011 Pedro Algarvio <ufs@ufsoft.org>
|
|
#
|
|
# Deluge is free software.
|
|
#
|
|
# You may redistribute it and/or modify it under the terms of the
|
|
# GNU General Public License, as published by the Free Software
|
|
# Foundation; either version 3 of the License, or (at your option)
|
|
# any later version.
|
|
#
|
|
# deluge 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 General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with deluge. If not, write to:
|
|
# The Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor
|
|
# Boston, MA 02110-1301, USA.
|
|
#
|
|
# In addition, as a special exception, the copyright holders give
|
|
# permission to link the code of portions of this program with the OpenSSL
|
|
# library.
|
|
# You must obey the GNU General Public License in all respects for all of
|
|
# the code used other than OpenSSL. If you modify file(s) with this
|
|
# exception, you may extend this exception to your version of the file(s),
|
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
|
# this exception statement from your version. If you delete this exception
|
|
# statement from all source files in the program, then also delete it here.
|
|
#
|
|
#
|
|
|
|
import os
|
|
import random
|
|
import stat
|
|
import shutil
|
|
import logging
|
|
|
|
import deluge.component as component
|
|
import deluge.configmanager as configmanager
|
|
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
AUTH_LEVEL_NONE = 0
|
|
AUTH_LEVEL_READONLY = 1
|
|
AUTH_LEVEL_NORMAL = 5
|
|
AUTH_LEVEL_ADMIN = 10
|
|
|
|
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
|
|
|
AUTH_LEVELS_MAPPING = {
|
|
'NONE': AUTH_LEVEL_NONE,
|
|
'READONLY': AUTH_LEVEL_READONLY,
|
|
'DEFAULT': AUTH_LEVEL_NORMAL,
|
|
'NORMAL': AUTH_LEVEL_DEFAULT,
|
|
'ADMIN': AUTH_LEVEL_ADMIN
|
|
}
|
|
|
|
AUTH_LEVELS_MAPPING_REVERSE = {}
|
|
for key, value in AUTH_LEVELS_MAPPING.iteritems():
|
|
AUTH_LEVELS_MAPPING_REVERSE[value] = key
|
|
|
|
class Account(object):
|
|
__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)s" authlevel=%(authlevel)s>' %
|
|
self.__dict__)
|
|
|
|
|
|
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):
|
|
log.debug("Querying for changed auth file")
|
|
auth_file = configmanager.get_config_dir("auth")
|
|
# Check for auth file and create if necessary
|
|
if not os.path.exists(auth_file):
|
|
log.info("Authfile not found, recreating it.")
|
|
self.__create_auth_file()
|
|
self.__create_localclient_account()
|
|
self.write_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
|
|
|
|
log.info("auth file changed, reloading it!")
|
|
self.__load_auth_file()
|
|
|
|
def authorize(self, username, password):
|
|
"""
|
|
Authorizes users based on username and password
|
|
|
|
:param username: str, username
|
|
:param password: str, password
|
|
:returns: int, the auth level for this user
|
|
:rtype: int
|
|
|
|
:raises AuthenticationRequired: if aditional details are required to
|
|
authenticate.
|
|
:raises 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)
|
|
try:
|
|
self.__auth[username] = Account(username, password,
|
|
AUTH_LEVELS_MAPPING[authlevel])
|
|
self.write_auth_file()
|
|
return True
|
|
except Exception, err:
|
|
log.exception(err)
|
|
raise err
|
|
|
|
def update_account(self, username, password, authlevel):
|
|
if username not in self.__auth:
|
|
raise AuthManagerError("Username not known", username)
|
|
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, err:
|
|
log.exception(err)
|
|
raise err
|
|
|
|
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):
|
|
old_auth_file = configmanager.get_config_dir("auth")
|
|
new_auth_file = old_auth_file + '.new'
|
|
bak_auth_file = old_auth_file + '.bak'
|
|
# Let's first create a backup
|
|
shutil.copy2(old_auth_file, bak_auth_file)
|
|
|
|
try:
|
|
fd = open(new_auth_file, "w")
|
|
for account in self.__auth.values():
|
|
fd.write(
|
|
"%(username)s:%(password)s:%(authlevel_int)s\n" %
|
|
account.data()
|
|
)
|
|
fd.flush()
|
|
os.fsync(fd.fileno())
|
|
fd.close()
|
|
os.rename(new_auth_file, old_auth_file)
|
|
except:
|
|
# Something failed, let's restore the previous file
|
|
os.rename(bak_auth_file, old_auth_file)
|
|
|
|
self.__load_auth_file()
|
|
|
|
def __create_localclient_account(self):
|
|
"""
|
|
Returns the string.
|
|
"""
|
|
# We create a 'localclient' account with a random password
|
|
try:
|
|
from hashlib import sha1 as sha_hash
|
|
except ImportError:
|
|
from sha import new as sha_hash
|
|
self.__auth["localclient"] = Account(
|
|
"localclient",
|
|
sha_hash(str(random.random())).hexdigest(),
|
|
AUTH_LEVEL_ADMIN
|
|
)
|
|
|
|
def __load_auth_file(self):
|
|
save_and_reload = False
|
|
auth_file = configmanager.get_config_dir("auth")
|
|
# Check for auth file and create if necessary
|
|
if not os.path.exists(auth_file):
|
|
self.__create_auth_file()
|
|
self.__create_localclient_account()
|
|
self.write_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
|
|
|
|
# Load the auth file into a dictionary: {username: Account(...)}
|
|
f = open(auth_file, "r").readlines()
|
|
|
|
for line in f:
|
|
if line.startswith("#"):
|
|
# This is a comment line
|
|
continue
|
|
line = line.strip()
|
|
try:
|
|
lsplit = line.split(":")
|
|
except Exception, e:
|
|
log.error("Your auth file is malformed: %s", e)
|
|
continue
|
|
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)
|
|
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:
|
|
self.__create_localclient_account()
|
|
self.write_auth_file()
|
|
|
|
if save_and_reload:
|
|
log.info("Re-writing auth file (upgrade)")
|
|
self.write_auth_file()
|
|
|
|
|
|
def __create_auth_file(self):
|
|
auth_file = configmanager.get_config_dir("auth")
|
|
# Check for auth file and create if necessary
|
|
if not os.path.exists(auth_file):
|
|
fd = open(auth_file, "w")
|
|
fd.flush()
|
|
os.fsync(fd.fileno())
|
|
fd.close()
|
|
# Change the permissions on the file so only this user can read/write it
|
|
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)
|