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
class AuthError(Exception):
"""
An exception that might be raised when checking a request for
authentication.
"""
pass
import time
import random
import hashlib
import logging
from twisted.internet.defer import Deferred
from twisted.internet.task import LoopingCall
from deluge import component
from deluge.ui.web.json_api import JSONComponent, export
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):
"""
The component that implements authentification into the JSON interface.
@ -59,13 +88,31 @@ class Auth(JSONComponent):
def __init__(self):
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.
:keyword login: the username of the user logging in, currently \
only for future use.
only for future use currently.
:type login: string
"""
m = hashlib.md5()
@ -75,6 +122,16 @@ class Auth(JSONComponent):
m.update(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)
config = component.get("DelugeWeb").config
@ -82,9 +139,54 @@ class Auth(JSONComponent):
config.config["sessions"] = {}
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
def change_password(self, new_password):
@ -104,25 +206,21 @@ class Auth(JSONComponent):
config["pwd_sha1"] = s.hexdigest()
d.callback(True)
return d
@export
def check_session(self, session_id):
@export(AUTH_LEVEL_NONE)
def check_session(self, session_id=None):
"""
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.
:rtype: booleon
"""
d = Deferred()
config = component.get("DelugeWeb").config
d.callback(session_id in config["sessions"])
d.callback(__request__.session_id is not None)
return d
@export
def delete_session(self, session_id):
def delete_session(self):
"""
Removes a session.
@ -131,11 +229,11 @@ class Auth(JSONComponent):
"""
d = Deferred()
config = component.get("DelugeWeb").config
del config["sessions"][session_id]
del config["sessions"][__request__.session_id]
d.callback(True)
return d
@export
@export(AUTH_LEVEL_NONE)
def login(self, password):
"""
Test a password to see if it's valid.
@ -177,7 +275,7 @@ class Auth(JSONComponent):
m.update(password)
if m.digest() == decodestring(config["old_pwd_md5"]):
# 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
# the old passwords from the config file.
@ -193,7 +291,7 @@ class Auth(JSONComponent):
s.update(password)
if s.hexdigest() == config["pwd_sha1"]:
# 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:
# Can't detect which method we should be using so just deny

View File

@ -103,28 +103,34 @@ Ext.namespace('Ext.ux.util');
errorObj = {
id: options.id,
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 (options.scope) {
options.failure.call(options.scope, errorObj, response, requestOptions);
} else {
options.failure(errorObj, response, requestOptions);
}
this.fireEvent('error', errorObj, response, requestOptions)
}
},
_onSuccess: function(response, requestOptions) {
var responseObj = Ext.decode(response.responseText);
var options = requestOptions.options;
if (responseObj.error) {
this.fireEvent('error', responseObj, response, requestOptions);
if (Ext.type(options.failure) != 'function') return;
if (options.scope) {
options.failure.call(options.scope, responseObj, response, requestOptions);
} else {
options.failure(responseObj, response, requestOptions);
}
this.fireEvent('error', responseObj, response, requestOptions)
} else {
if (Ext.type(options.success) != 'function') return;
if (options.scope) {

View File

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

View File

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

View File

@ -56,6 +56,7 @@ json = common.json
log = logging.getLogger(__name__)
AUTH_LEVEL_DEFAULT = None
AuthError = None
class JSONComponent(component.Component):
def __init__(self, name, interval=1, depend=None):
@ -74,9 +75,9 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
:type auth_level: int
"""
global AUTH_LEVEL_DEFAULT
global AUTH_LEVEL_DEFAULT, AuthError
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):
func._json_export = True
@ -153,6 +154,7 @@ class JSON(resource.Resource, component.Component):
# and any plugins.
meth = self._local_methods[method]
meth.func_globals['__request__'] = request
component.get("Auth").check_request(request, meth)
return meth(*params)
raise JSONException("Unknown system method")
@ -160,6 +162,7 @@ class JSON(resource.Resource, component.Component):
"""
Executes methods using the Deluge client.
"""
component.get("Auth").check_request(request, level=AUTH_LEVEL_DEFAULT)
component, method = method.split(".")
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
procedure calls and the request id.
"""
request_id = None
try:
request.json = json.loads(request.json)
except ValueError:
@ -181,20 +183,25 @@ class JSON(resource.Resource, component.Component):
method, params = request.json["method"], request.json["params"]
request_id = request.json["id"]
result = None
error = None
try:
if method.startswith("system."):
return self._exec_local(method, params, request), request_id
elif method in self._local_methods:
return self._exec_local(method, params, request), request_id
if method.startswith("system.") or method in self._local_methods:
result = self._exec_local(method, params, request)
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:
log.error("Error calling method `%s`", method)
log.exception(e)
d = Deferred()
d.callback(None)
return d, request_id
error = {"message": e.message, "code": 3}
return request_id, result, error
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.
"""
print type(reason)
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
return ""
@ -218,10 +224,14 @@ class JSON(resource.Resource, component.Component):
"""
log.debug("json-request: %s", request.json)
response = {"result": None, "error": None, "id": None}
d, response["id"] = self._handle_request(request)
d.addCallback(self._on_rpc_request_finished, response, request)
d.addErrback(self._on_rpc_request_failed, response, request)
return d
response["id"], d, response["error"] = self._handle_request(request)
if isinstance(d, Deferred):
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):
"""