burp-ui/burpui/misc/auth/handler.py
2023-03-19 15:13:01 +01:00

340 lines
11 KiB
Python

# -*- coding: utf8 -*-
import os
from collections import OrderedDict
from importlib import import_module
from flask import session
from flask_login import AnonymousUserMixin
from ...sessions import session_manager
from ...utils import is_uuid
from ..acl.interface import BUIacl
from .interface import BUIhandler, BUIuser
ACL_METHODS = BUIacl.__abstractmethods__
class UserAuthHandler(BUIhandler):
"""See :class:`burpui.misc.auth.interface.BUIhandler`"""
def __init__(self, app=None):
"""See :func:`burpui.misc.auth.interface.BUIhandler.__init__`
:param app: Instance of the app we are running in
:type app: :class:`burpui.engines.server.BUIServer`
"""
self.app = app
self.users = {}
backends = []
self.errors = {}
if self.app.auth and "none" not in self.app.auth:
me, _ = os.path.splitext(os.path.basename(__file__))
back = self.app.auth
for au in back:
if au == me:
self.app.logger.critical("Recursive import not permitted!")
continue
try:
(modpath, _) = __name__.rsplit(".", 1)
mod = import_module("." + au, modpath)
obj = mod.UserHandler(self.app)
backends.append(obj)
except:
import traceback
self.errors[au] = traceback.format_exc()
for name, plugin in self.app.plugin_manager.get_plugins_by_type("auth").items():
try:
obj = plugin.UserHandler(self.app)
backends.append(obj)
except:
import traceback
self.errors[name] = traceback.format_exc()
backends.sort(key=lambda x: getattr(x, "priority", -1), reverse=True)
if not backends:
raise ImportError(
"No backend found for '{}':\n{}".format(self.app.auth, self.errors)
)
for name, err in self.errors.items():
self.app.logger.error(
"Unable to load module {}:\n{}".format(repr(name), err)
)
self.backends = OrderedDict()
for obj in backends:
self.backends[obj.name] = obj
def user(self, name=None, refresh=False):
"""See :func:`burpui.misc.auth.interface.BUIhandler.user`"""
key = session_manager.get_session_id() or name
if (
key != name
and is_uuid(name)
and name in self.users
and not session_manager.session_expired_by_id(name)
):
usr = self.users[name]
usr.id = key
self.users[key] = usr
del self.users[name]
session_manager.session_import_from(name)
session["authenticated"] = True
if not key:
return None
if refresh and key in self.users:
del self.users[key]
if session_manager.session_managed():
session_manager.session_expired()
if key not in self.users:
ret = UserHandler(self.app, self.backends, name, key, refresh)
if not ret.name or not ret.active:
return None
self.users[key] = ret
return ret
ret = self.users[key]
ret.refresh_session()
self.users[key] = ret
return ret
def remove(self, name):
"""See :func:`burpui.misc.auth.interface.BUIhandler.remove`"""
if name in self.users:
del self.users[name]
@property
def loader(self):
return None
class ProxyACLCall(object):
"""Class that actually calls the ACL method"""
def __init__(self, acl, username, method):
"""
:param acl: ACL to use
:type acl: :class:`burpui.misc.acl.interface.BUIacl`
:param username: username to check ACL for
:type username: str
:param method: Name of the method to proxify
:type method: str
"""
self.acl = acl
self.username = username
self.method = method
def __call__(self, *args, **kwargs):
"""This is where the proxy call (and the magic) occurs"""
# retrieve the original function prototype
proto = getattr(BUIacl, self.method)
args_name = list(proto.__code__.co_varnames)
# skip self
args_name.pop(0)
# skip username
args_name.pop(0)
# we transform unnamed arguments to named ones
# example:
# def my_function(toto, tata=None, titi=None):
#
# x = my_function('blah', titi='blih')
#
# => {'toto': 'blah', 'titi': 'blih'}
encoded_args = {"username": self.username}
for idx, opt in enumerate(args):
encoded_args[args_name[idx]] = opt
encoded_args.update(kwargs)
func = getattr(self.acl, self.method)
return func(**encoded_args)
class ACLproxy(BUIacl):
foreign = ACL_METHODS
BUIacl.__abstractmethods__ = frozenset()
def __init__(self, acl, username):
self.acl = acl
self.username = username
def __getattribute__(self, name):
# always return this value because we need it and if we don't do that
# we'll end up with an infinite loop
if name in ["foreign", "acl", "username"]:
return object.__getattribute__(self, name)
# now we can retrieve the 'foreign' list and know if the object called
# needs to be "proxyfied"
if name in self.foreign:
if self.acl:
return ProxyACLCall(self.acl, self.username, name)
# no ACL, assume true
return ProxyTrue()
return object.__getattribute__(self, name)
class ProxyTrue(object):
def __call__(self, *args, **kwargs):
return True
class ProxyFalse(object):
def __call__(self, *args, **kwargs):
return False
class ACLanon(BUIacl):
foreign = ACL_METHODS
BUIacl.__abstractmethods__ = frozenset()
def __getattribute__(self, name):
# always return this value because we need it and if we don't do that
# we'll end up with an infinite loop
if name == "foreign":
return object.__getattribute__(self, name)
# now we can retrieve the 'foreign' list and know if the object called
# needs to be "proxyfied"
if name in self.foreign:
return ProxyFalse()
return object.__getattribute__(self, name)
class BUIanon(AnonymousUserMixin):
_acl = ACLanon()
name = "Unknown"
def login(self, passwd=None):
return False
@property
def acl(self):
return self._acl
@property
def is_admin(self):
return False
@property
def is_moderator(self):
return False
class UserHandler(BUIuser):
"""See :class:`burpui.misc.auth.interface.BUIuser`"""
def __init__(self, app, backends=None, name=None, id=None, refresh=False):
"""
:param app: Application context
:type app: :class:`burpui.engines.server.BUIServer`
"""
self.id = id
self.app = app
self.active = False
last_backend = session.get("backend", None)
self.authenticated = session.get("authenticated", False)
self.language = session.get("language", None)
self.backends = backends
self.back = None
if refresh or not is_uuid(name):
username = name
else:
username = session_manager.get_session_username() or session.get("login")
self.real = None
self.name = username
if not self.name:
return
def _load_from_backend(backend, username):
if not backend:
return False
user = backend.user(username)
if not user:
return False
ext = user.get_id()
if ext:
self.real = user
self.active = True
self.name = ext
self.back = backend
return True
return False
if refresh:
for _, back in self.backends.items():
if _load_from_backend(back, self.name):
break
elif last_backend:
back = self.backends.get(last_backend)
_load_from_backend(back, self.name)
self._acl = ACLproxy(self.app.acl, self.name)
# now load the available prefs
self._load_prefs()
@property
def acl(self):
return self._acl
@property
def is_admin(self):
return self.acl.is_admin()
@property
def is_moderator(self):
return self.acl.is_moderator()
@property
def backend(self):
return getattr(self.real, "backend", None)
def _load_prefs(self):
session["login"] = self.name
if self.app.config["WITH_SQL"]:
from ...models import Pref
prefs = Pref.query.filter_by(user=self.name).all()
for pref in prefs:
if pref.key == "language":
continue
if hasattr(self, pref.key):
setattr(self, pref.key, pref.value)
session[pref.key] = pref.value
def refresh_session(self):
self.authenticated = session.get("authenticated", False)
self.language = session.get("language", None)
self._load_prefs()
def login(self, passwd=None):
"""See :func:`burpui.misc.auth.interface.BUIuser.login`"""
if not self.real:
self.authenticated = False
for name, back in self.backends.items():
user = back.user(self.name)
if not user:
continue
res = user.get_id()
if user.login(passwd):
self.authenticated = True
self.real = user
self.back = back
self.name = res
self._acl.username = res
break
elif self.real: # pragma: no cover
if self.back and getattr(self.back, "changed", False):
self.real = None
self.back = None
return self.login(passwd)
self.authenticated = self.real.login(passwd)
# allow to try another backend
if not self.authenticated:
self.real = None
self.back = None
return self.login(passwd)
session["authenticated"] = self.authenticated
session["language"] = self.language
session["login"] = self.name
session["backend"] = self.backend
return self.authenticated