complete the new auth system using cookies & sessions

This commit is contained in:
Damien Churchill 2009-08-11 23:39:58 +00:00
parent 1b17d66576
commit 3e8c17b071
5 changed files with 190 additions and 73 deletions

View File

@ -40,18 +40,47 @@ AUTH_LEVEL_ADMIN = 10
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
class AuthError(Exception):
"""
An exception that might be raised when checking a request for
authentication.
"""
pass
import time import time
import random import random
import hashlib import hashlib
import logging import logging
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from twisted.internet.task import LoopingCall
from deluge import component from deluge import component
from deluge.ui.web.json_api import JSONComponent, export from deluge.ui.web.json_api import JSONComponent, export
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def make_checksum(session_id):
return reduce(lambda x,y:x+y, map(ord, session_id))
def get_session_id(session_id):
"""
Checks a session id against its checksum
"""
if not session_id:
return None
try:
checksum = int(session_id[-4:])
session_id = session_id[:-4]
if checksum == make_checksum(session_id):
return session_id
return None
except Exception, e:
log.exception(e)
return None
class Auth(JSONComponent): class Auth(JSONComponent):
""" """
The component that implements authentification into the JSON interface. The component that implements authentification into the JSON interface.
@ -59,13 +88,31 @@ class Auth(JSONComponent):
def __init__(self): def __init__(self):
super(Auth, self).__init__("Auth") super(Auth, self).__init__("Auth")
self.worker = LoopingCall(self._clean_sessions)
self.worker.start(5)
def _create_session(self, login='admin'): def _clean_sessions(self):
config = component.get("DelugeWeb").config
session_ids = config["sessions"].keys()
now = time.gmtime()
for session_id in session_ids:
session = config["sessions"][session_id]
if "expires" not in session:
del config["sessions"][session_id]
continue
if time.gmtime(session["expires"]) < now:
del config["sessions"][session_id]
continue
def _create_session(self, request, login='admin'):
""" """
Creates a new session. Creates a new session.
:keyword login: the username of the user logging in, currently \ :keyword login: the username of the user logging in, currently \
only for future use. only for future use currently.
:type login: string :type login: string
""" """
m = hashlib.md5() m = hashlib.md5()
@ -75,6 +122,16 @@ class Auth(JSONComponent):
m.update(m.hexdigest()) m.update(m.hexdigest())
session_id = m.hexdigest() session_id = m.hexdigest()
expires = int(time.time()) + 3600
expires_str = time.strftime('%a, %d %b %Y %H:%M:%S UTC',
time.gmtime(expires))
checksum = str(make_checksum(session_id))
print 'Adding cookie'
request.addCookie('_session_id', session_id + checksum,
path="/json", expires=expires_str)
log.debug("Creating session for %s", login) log.debug("Creating session for %s", login)
config = component.get("DelugeWeb").config config = component.get("DelugeWeb").config
@ -82,9 +139,54 @@ class Auth(JSONComponent):
config.config["sessions"] = {} config.config["sessions"] = {}
config["sessions"][session_id] = { config["sessions"][session_id] = {
"login": login "login": login,
"level": AUTH_LEVEL_ADMIN,
"expires": expires
} }
return session_id return True
def check_request(self, request, method=None, level=None):
"""
Check to ensure that a request is authorised to call the specified
method of authentication level.
:param request: The HTTP request in question
:type request: twisted.web.http.Request
:keyword method: Check the specified method
:type method: function
:keyword level: Check the specified auth level
:type level: integer
:raises: Exception
"""
config = component.get("DelugeWeb").config
session_id = get_session_id(request.getCookie("_session_id"))
if session_id not in config["sessions"]:
auth_level = AUTH_LEVEL_NONE
session_id = None
else:
auth_level = config["sessions"][session_id]["level"]
if method:
if not hasattr(method, "_json_export"):
raise Exception("Not an exported method")
method_level = getattr(method, "_json_auth_level")
if method_level is None:
raise Exception("Method has no auth level")
level = method_level
if level is None:
raise Exception("No level specified to check against")
request.auth_level = auth_level
request.session_id = session_id
if auth_level < level:
raise AuthError("Not authenticated")
@export @export
def change_password(self, new_password): def change_password(self, new_password):
@ -104,25 +206,21 @@ class Auth(JSONComponent):
config["pwd_sha1"] = s.hexdigest() config["pwd_sha1"] = s.hexdigest()
d.callback(True) d.callback(True)
return d return d
@export @export(AUTH_LEVEL_NONE)
def check_session(self, session_id): def check_session(self, session_id=None):
""" """
Check a session to see if it's still valid. Check a session to see if it's still valid.
:param session_id: the id for the session to remove
:type session_id: string
:returns: True if the session is valid, False if not. :returns: True if the session is valid, False if not.
:rtype: booleon :rtype: booleon
""" """
d = Deferred() d = Deferred()
config = component.get("DelugeWeb").config d.callback(__request__.session_id is not None)
d.callback(session_id in config["sessions"])
return d return d
@export @export
def delete_session(self, session_id): def delete_session(self):
""" """
Removes a session. Removes a session.
@ -131,11 +229,11 @@ class Auth(JSONComponent):
""" """
d = Deferred() d = Deferred()
config = component.get("DelugeWeb").config config = component.get("DelugeWeb").config
del config["sessions"][session_id] del config["sessions"][__request__.session_id]
d.callback(True) d.callback(True)
return d return d
@export @export(AUTH_LEVEL_NONE)
def login(self, password): def login(self, password):
""" """
Test a password to see if it's valid. Test a password to see if it's valid.
@ -177,7 +275,7 @@ class Auth(JSONComponent):
m.update(password) m.update(password)
if m.digest() == decodestring(config["old_pwd_md5"]): if m.digest() == decodestring(config["old_pwd_md5"]):
# We have a match, so we can create and return a session id. # We have a match, so we can create and return a session id.
d.callback(self._create_session()) d.callback(self._create_session(__request__))
# We also want to move the password over to sha1 and remove # We also want to move the password over to sha1 and remove
# the old passwords from the config file. # the old passwords from the config file.
@ -193,7 +291,7 @@ class Auth(JSONComponent):
s.update(password) s.update(password)
if s.hexdigest() == config["pwd_sha1"]: if s.hexdigest() == config["pwd_sha1"]:
# We have a match, so we can create and return a session id. # We have a match, so we can create and return a session id.
d.callback(self._create_session()) d.callback(self._create_session(__request__))
else: else:
# Can't detect which method we should be using so just deny # Can't detect which method we should be using so just deny

View File

@ -103,28 +103,34 @@ Ext.namespace('Ext.ux.util');
errorObj = { errorObj = {
id: options.id, id: options.id,
result: null, result: null,
error: 'HTTP: ' + response.status + ' ' + response.statusText error: {
} msg: 'HTTP: ' + response.status + ' ' + response.statusText,
code: 255
}
}
this.fireEvent('error', errorObj, response, requestOptions)
if (Ext.type(options.failure) != 'function') return; if (Ext.type(options.failure) != 'function') return;
if (options.scope) { if (options.scope) {
options.failure.call(options.scope, errorObj, response, requestOptions); options.failure.call(options.scope, errorObj, response, requestOptions);
} else { } else {
options.failure(errorObj, response, requestOptions); options.failure(errorObj, response, requestOptions);
} }
this.fireEvent('error', errorObj, response, requestOptions)
}, },
_onSuccess: function(response, requestOptions) { _onSuccess: function(response, requestOptions) {
var responseObj = Ext.decode(response.responseText); var responseObj = Ext.decode(response.responseText);
var options = requestOptions.options; var options = requestOptions.options;
if (responseObj.error) { if (responseObj.error) {
this.fireEvent('error', responseObj, response, requestOptions);
if (Ext.type(options.failure) != 'function') return; if (Ext.type(options.failure) != 'function') return;
if (options.scope) { if (options.scope) {
options.failure.call(options.scope, responseObj, response, requestOptions); options.failure.call(options.scope, responseObj, response, requestOptions);
} else { } else {
options.failure(responseObj, response, requestOptions); options.failure(responseObj, response, requestOptions);
} }
this.fireEvent('error', responseObj, response, requestOptions)
} else { } else {
if (Ext.type(options.success) != 'function') return; if (Ext.type(options.success) != 'function') return;
if (options.scope) { if (options.scope) {

View File

@ -57,9 +57,7 @@ Copyright:
initComponent: function() { initComponent: function() {
Ext.deluge.LoginWindow.superclass.initComponent.call(this); Ext.deluge.LoginWindow.superclass.initComponent.call(this);
Deluge.Events.on('logout', this.onLogout, this);
this.on('show', this.onShow, this); this.on('show', this.onShow, this);
this.on('beforeshow', this.onBeforeShow, this);
this.addButton({ this.addButton({
text: _('Login'), text: _('Login'),
@ -89,6 +87,33 @@ Copyright:
}) })
}, },
show: function(skipCheck) {
if (this.firstShow) {
Deluge.Client.on('error', this.onClientError, this);
this.firstShow = false;
}
if (skipCheck) {
return Ext.deluge.LoginWindow.superclass.show.call(this);
}
Deluge.Client.auth.check_session({
success: function(result) {
if (result) {
Deluge.Events.fire('login');
this.loginForm.items.get('password').setRawValue('');
this.hide();
} else {
this.show(true);
}
},
failure: function(result) {
this.show(true);
},
scope: this
});
},
onKey: function(field, e) { onKey: function(field, e) {
if (e.getKey() == 13) this.onLogin(); if (e.getKey() == 13) this.onLogin();
}, },
@ -101,7 +126,6 @@ Copyright:
Deluge.Events.fire('login'); Deluge.Events.fire('login');
this.hide(); this.hide();
passwordField.setRawValue(''); passwordField.setRawValue('');
Deluge.UI.cookies.set("session", result);
} else { } else {
Ext.MessageBox.show({ Ext.MessageBox.show({
title: _('Login Failed'), title: _('Login Failed'),
@ -121,39 +145,19 @@ Copyright:
}, },
onLogout: function() { onLogout: function() {
var session = Deluge.UI.cookies.get("session", false); Deluge.Events.fire('logout');
if (session) { Deluge.Client.auth.delete_session({
Deluge.Client.auth.delete_session(session, { success: function(result) {
success: function(result) { this.show(true);
Deluge.UI.cookies.clear("session"); },
this.show(); scope: this
}, });
scope: this
});
}
}, },
onBeforeShow: function() { onClientError: function(errorObj, response, requestOptions) {
var session = Deluge.UI.cookies.get("session", false); if (errorObj.error.code == 1) {
if (session) { Deluge.Events.fire('logout');
Deluge.Client.auth.check_session(session, { this.show(true);
success: function(result) {
if (result) {
Deluge.Events.fire('login');
this.loginForm.items.get('password').setRawValue('');
this.hide();
} else {
Deluge.UI.cookies.clear("session");
this.show();
}
},
failure: function(result) {
Deluge.UI.cookies.clear("session");
this.show();
},
scope: this
});
return false;
} }
}, },

View File

@ -150,8 +150,7 @@ Copyright:
onLogout: function() { onLogout: function() {
this.items.get('logout').disable(); this.items.get('logout').disable();
Deluge.Events.fire('logout'); Deluge.Login.logout();
Deluge.Login.show();
}, },
onConnectionManagerClick: function() { onConnectionManagerClick: function() {

View File

@ -56,6 +56,7 @@ json = common.json
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
AUTH_LEVEL_DEFAULT = None AUTH_LEVEL_DEFAULT = None
AuthError = None
class JSONComponent(component.Component): class JSONComponent(component.Component):
def __init__(self, name, interval=1, depend=None): def __init__(self, name, interval=1, depend=None):
@ -74,9 +75,9 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
:type auth_level: int :type auth_level: int
""" """
global AUTH_LEVEL_DEFAULT global AUTH_LEVEL_DEFAULT, AuthError
if AUTH_LEVEL_DEFAULT is None: if AUTH_LEVEL_DEFAULT is None:
from deluge.ui.web.auth import AUTH_LEVEL_DEFAULT from deluge.ui.web.auth import AUTH_LEVEL_DEFAULT, AuthError
def wrap(func, *args, **kwargs): def wrap(func, *args, **kwargs):
func._json_export = True func._json_export = True
@ -153,6 +154,7 @@ class JSON(resource.Resource, component.Component):
# and any plugins. # and any plugins.
meth = self._local_methods[method] meth = self._local_methods[method]
meth.func_globals['__request__'] = request meth.func_globals['__request__'] = request
component.get("Auth").check_request(request, meth)
return meth(*params) return meth(*params)
raise JSONException("Unknown system method") raise JSONException("Unknown system method")
@ -160,6 +162,7 @@ class JSON(resource.Resource, component.Component):
""" """
Executes methods using the Deluge client. Executes methods using the Deluge client.
""" """
component.get("Auth").check_request(request, level=AUTH_LEVEL_DEFAULT)
component, method = method.split(".") component, method = method.split(".")
return getattr(getattr(client, component), method)(*params) return getattr(getattr(client, component), method)(*params)
@ -169,7 +172,6 @@ class JSON(resource.Resource, component.Component):
the rpc object that should be contained, returning a deferred for all the rpc object that should be contained, returning a deferred for all
procedure calls and the request id. procedure calls and the request id.
""" """
request_id = None
try: try:
request.json = json.loads(request.json) request.json = json.loads(request.json)
except ValueError: except ValueError:
@ -181,20 +183,25 @@ class JSON(resource.Resource, component.Component):
method, params = request.json["method"], request.json["params"] method, params = request.json["method"], request.json["params"]
request_id = request.json["id"] request_id = request.json["id"]
result = None
error = None
try: try:
if method.startswith("system."): if method.startswith("system.") or method in self._local_methods:
return self._exec_local(method, params, request), request_id result = self._exec_local(method, params, request)
elif method in self._local_methods:
return self._exec_local(method, params, request), request_id
elif method in self._remote_methods: elif method in self._remote_methods:
return self._exec_remote(method, params), request_id result = self._exec_remote(method, params)
else:
error = {"message": "Unknown method", "code": 2}
except AuthError, e:
error = {"message": "Not authenticated", "code": 1}
except Exception, e: except Exception, e:
log.error("Error calling method `%s`", method) log.error("Error calling method `%s`", method)
log.exception(e) log.exception(e)
d = Deferred()
d.callback(None) error = {"message": e.message, "code": 3}
return d, request_id
return request_id, result, error
def _on_rpc_request_finished(self, result, response, request): def _on_rpc_request_finished(self, result, response, request):
""" """
@ -207,7 +214,6 @@ class JSON(resource.Resource, component.Component):
""" """
Handles any failures that occured while making an rpc call. Handles any failures that occured while making an rpc call.
""" """
print type(reason)
request.setResponseCode(http.INTERNAL_SERVER_ERROR) request.setResponseCode(http.INTERNAL_SERVER_ERROR)
return "" return ""
@ -218,10 +224,14 @@ class JSON(resource.Resource, component.Component):
""" """
log.debug("json-request: %s", request.json) log.debug("json-request: %s", request.json)
response = {"result": None, "error": None, "id": None} response = {"result": None, "error": None, "id": None}
d, response["id"] = self._handle_request(request) response["id"], d, response["error"] = self._handle_request(request)
d.addCallback(self._on_rpc_request_finished, response, request)
d.addErrback(self._on_rpc_request_failed, response, request) if isinstance(d, Deferred):
return d d.addCallback(self._on_rpc_request_finished, response, request)
d.addErrback(self._on_rpc_request_failed, response, request)
return d
else:
return self._send_response(request, response)
def _on_json_request_failed(self, reason, request): def _on_json_request_failed(self, reason, request):
""" """