diff --git a/burpui/api/admin.py b/burpui/api/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..470bc8171f4f302b38f043b528fe8ecfceffa2be --- /dev/null +++ b/burpui/api/admin.py @@ -0,0 +1,291 @@ +# -*- coding: utf8 -*- +""" +.. module:: burpui.api.misc + :platform: Unix + :synopsis: Burp-UI admin api module. + +.. moduleauthor:: Ziirish <hi+burpui@ziirish.me> + +""" +from . import api +from .custom import fields, Resource +# from ..exceptions import BUIserverException + +from six import iteritems + +ns = api.namespace('admin', 'Admin methods') + + +@ns.route('/auth/users', + '/auth/users/<name>', + endpoint='auth_users') +class AuthUsers(Resource): + """The :class:`burpui.api.admin.AuthUsers` resource allows you to + retrieve a list of users and to add/update/delete them if your + authentication backend support those actions. + + This resource is part of the :mod:`burpui.api.admin` module. + """ + user_fields = ns.model('Users', { + 'id': fields.String(required=True, description='User id'), + 'name': fields.String(required=True, description='User name'), + 'backend': fields.String(required=True, description='Backend name'), + }) + parser_add = ns.parser() + parser_add.add_argument('name', required=True, help='Username') + parser_add.add_argument('password', required=True, help='Password') + parser_add.add_argument('backend', required=True, help='Backend') + + parser_mod = ns.parser() + parser_mod.add_argument('password', required=True, help='Password') + parser_mod.add_argument('backend', required=True, help='Backend') + + parser_del = ns.parser() + parser_del.add_argument('backend', required=True, help='Backend') + + @ns.marshal_list_with(user_fields, code=200, description='Success') + @ns.doc( + responses={ + 403: 'Insufficient permissions', + 404: 'No backend found', + }, + ) + def get(self): + """Returns a list of users + + **GET** method provided by the webservice. + + :returns: Users + """ + # Manage ACL + if not api.bui.acl or not self.is_admin: + self.abort(403, "Not allowed to view users list") + + try: + handler = getattr(api.bui, 'uhandler') + except AttributeError: + handler = None + + if not handler or len(handler.backends) == 0: + self.abort(404, "No authentication backend found") + ret = [] + for backend in handler.backends: + loader = backend.loader + try: + users = getattr(loader, 'users') + except AttributeError: + continue + if users: + if isinstance(users, list): + for user in users: + ret.append({ + 'id': backend.user(user).get_id(), + 'name': user, + 'backend': backend.name + }) + elif isinstance(users, dict): + for user, _ in iteritems(users): + ret.append({ + 'id': backend.user(user).get_id(), + 'name': user, + 'backend': backend.name + }) + return ret + + @ns.expect(parser_add) + @ns.doc( + responses={ + 200: 'Request performed with errors', + 201: 'Success', + 403: 'Not allowed', + 400: 'Missing parameters', + 404: 'Backend not found', + 500: 'Backend does not support this operation', + }, + ) + def put(self): + """Create a new user""" + args = self.parser_add.parse_args() + # Manage ACL + if not api.bui.acl or not self.is_admin: + self.abort(403, "Not allowed to view users list") + + try: + handler = getattr(api.bui, 'uhandler') + except AttributeError: + handler = None + + if not handler or len(handler.backends) == 0: + self.abort(404, "No authentication backend found") + + backend = None + for back in handler.backends: + if back.name == args['backend']: + backend = back + break + + if not backend: + self.abort(404, "No authentication backend found") + + if backend.add_user is False: + self.abort( + 500, + "The '{}' backend does not support user creation" + "".format(args['backend']) + ) + + success, message, code = backend.add_user( + args['name'], + args['password'] + ) + status = 201 if success else 200 + return [[code, message]], status + + @ns.expect(parser_del) + @ns.doc( + responses={ + 200: 'Request performed with errors', + 201: 'Success', + 403: 'Not allowed', + 400: 'Missing parameters', + 404: 'Backend not found', + 500: 'Backend does not support this operation', + }, + ) + def delete(self, name): + """Change user password""" + args = self.parser_del.parse_args() + # Manage ACL + if not api.bui.acl or not self.is_admin: + self.abort(403, "Not allowed to view users list") + + try: + handler = getattr(api.bui, 'uhandler') + except AttributeError: + handler = None + + if not handler or len(handler.backends) == 0: + self.abort(404, "No authentication backend found") + + backend = None + for back in handler.backends: + if back.name == args['backend']: + backend = back + break + + if not backend: + self.abort(404, "No authentication backend found") + + if backend.del_user is False: + self.abort( + 500, + "The '{}' backend does not support user deletion" + "".format(args['backend']) + ) + + success, message, code = backend.del_user( + name + ) + status = 201 if success else 200 + return [[code, message]], status + + @ns.expect(parser_mod) + @ns.doc( + responses={ + 200: 'Request performed with errors', + 201: 'Success', + 403: 'Not allowed', + 400: 'Missing parameters', + 404: 'Backend not found', + 500: 'Backend does not support this operation', + }, + ) + def post(self, name): + """Delete a user""" + args = self.parser_mod.parse_args() + # Manage ACL + if not api.bui.acl or not self.is_admin: + self.abort(403, "Not allowed to view users list") + + try: + handler = getattr(api.bui, 'uhandler') + except AttributeError: + handler = None + + if not handler or len(handler.backends) == 0: + self.abort(404, "No authentication backend found") + + backend = None + for back in handler.backends: + if back.name == args['backend']: + backend = back + break + + if not backend: + self.abort(404, "No authentication backend found") + + if backend.change_password is False: + self.abort( + 500, + "The '{}' backend does not support user modification" + "".format(args['backend']) + ) + + success, message, code = backend.change_password( + name, + args['password'] + ) + status = 201 if success else 200 + return [[code, message]], status + + +@ns.route('/auth/backends', endpoint='auth_backends') +class AuthBackends(Resource): + """The :class:`burpui.api.admin.AuthBackends` resource allows you to + retrieve a list of backends and to add/update/delete users if your + authentication backend support those actions. + + This resource is part of the :mod:`burpui.api.admin` module. + """ + backend_fields = ns.model('Backends', { + 'name': fields.String(required=True, description='Backend name'), + 'add': fields.Boolean(required=False, default=False, description='Support user creation'), + 'mod': fields.Boolean(required=False, default=False, description='Support user edition'), + 'del': fields.Boolean(required=False, default=False, description='Support user deletion'), + }) + + @ns.marshal_list_with(backend_fields, code=200, description='Success') + @ns.doc( + responses={ + 403: 'Insufficient permissions', + 404: 'No backend found', + }, + ) + def get(self): + """Returns a list of backends + + **GET** method provided by the webservice. + + :returns: Backends + """ + # Manage ACL + if not api.bui.acl or not self.is_admin: + self.abort(403, "Not allowed to view users list") + + try: + handler = getattr(api.bui, 'uhandler') + except AttributeError: + handler = None + + if not handler or len(handler.backends) == 0: + self.abort(404, "No authentication backend found") + ret = [] + for backend in handler.backends: + ret.append({ + 'name': backend.name, + 'add': backend.add_user is not False, + 'del': backend.del_user is not False, + 'mod': backend.change_password is not False, + }) + + return ret diff --git a/burpui/misc/auth/basic.py b/burpui/misc/auth/basic.py index 05411a58dc53f4b5cd5d0668061fc3a4a2346631..b98b0aefae4486f33ca004aecf3d1dcaf218f7b6 100644 --- a/burpui/misc/auth/basic.py +++ b/burpui/misc/auth/basic.py @@ -3,6 +3,7 @@ import re import codecs from .interface import BUIhandler, BUIuser, BUIloader +from ...utils import NOTIF_ERROR, NOTIF_OK, NOTIF_WARN from werkzeug.security import check_password_hash, generate_password_hash @@ -25,6 +26,7 @@ class BasicLoader(BUIloader): self.app = app self.conf = self.app.conf self.handler = handler + self.handler.name = self.name self.handler.add_user = self.add_user self.handler.del_user = self.del_user self.handler.change_password = self.change_password @@ -139,42 +141,50 @@ class BasicLoader(BUIloader): """Add a user""" self._setup_users() if user in self.users: - self.logger.warning("user '{}' already exists".format(user)) - return False + message = "user '{}' already exists".format(user) + self.logger.warning(message) + return False, message, NOTIF_WARN pwd = generate_password_hash(passwd) self.conf.options[self.section][user] = pwd self.conf.options.write() self._load_users() - return True + message = "user '{}' successfully added".format(user) + return True, message, NOTIF_OK def del_user(self, user): """Delete a user""" self._setup_users() if user not in self.users: - self.logger.error("user '{}' does not exist".format(user)) - return False + message = "user '{}' does not exist".format(user) + self.logger.error(message) + return False, message, NOTIF_ERROR if user == 'admin' and len(self.users.keys()) == 1: - self.logger.warning('trying to delete the admin account!') - return False + message = 'trying to delete the admin account!' + self.logger.warning(message) + return False, message, NOTIF_WARN del self.conf.options[self.section][user] self.conf.options.write() self._load_users() - return True + message = "user '{}' successfully removed".format(user) + return True, message, NOTIF_OK def change_password(self, user, passwd): """Change a user password""" self._setup_users() if user not in self.users: - self.logger.error("user '{}' does not exist".format(user)) - return False + message = "user '{}' does not exist".format(user) + self.logger.error(message) + return False, message, NOTIF_ERROR if check_password_hash(self.users[user], passwd): - self.logger.warning('password is the same') - return False + message = 'password is the same' + self.logger.warning(message) + return False, message, NOTIF_WARN pwd = generate_password_hash(passwd) self.conf.options[self.section][user] = pwd self.conf.options.write() self._load_users() - return True + message = "user '{}' successfully updated".format(user) + return True, message, NOTIF_OK class UserHandler(BUIhandler): @@ -190,6 +200,10 @@ class UserHandler(BUIhandler): self.users[name] = BasicUser(self.basic, name) return self.users[name] + @property + def loader(self): + return self.basic + class BasicUser(BUIuser): """See :class:`burpui.misc.auth.interface.BUIuser`""" diff --git a/burpui/misc/auth/interface.py b/burpui/misc/auth/interface.py index 6c263158e9333e5d804fc6e01948f50fb8d195c3..6f5d7432f57b4be94a5a0bc90d9854ab8bec5372 100644 --- a/burpui/misc/auth/interface.py +++ b/burpui/misc/auth/interface.py @@ -27,6 +27,8 @@ class BUIhandler: priority = 0 + name = None + add_user = False del_user = False change_password = False @@ -48,6 +50,10 @@ class BUIhandler: """ return None # pragma: no cover + @property + def loader(self): + return None + class BUIuser(UserMixin): """The :class:`burpui.misc.auth.interface.BUIuser` class extends the diff --git a/burpui/misc/auth/ldap.py b/burpui/misc/auth/ldap.py index 060fcb1a19b3676461c4d55812712b1828a312a9..f1c72c673ce90667c4f624bd5e85432fb93c2924 100644 --- a/burpui/misc/auth/ldap.py +++ b/burpui/misc/auth/ldap.py @@ -26,6 +26,7 @@ class LdapLoader(BUIloader): """ self.app = app conf = self.app.conf + handler.name = self.name defaults = { 'LDAP': { 'host': 'localhost', @@ -202,6 +203,10 @@ class UserHandler(BUIhandler): self.users[name] = LdapUser(self.ldap, name) return self.users[name] + @property + def loader(self): + return self.ldap + class LdapUser(BUIuser): """The :class:`burpui.misc.auth.ldap.LdapUser` class generates a ``Burp-UI`` diff --git a/burpui/misc/auth/local.py b/burpui/misc/auth/local.py index 89a621c3c6fb8cdefb963b9b9ff0907e3dd9a6b1..2a25793beaeb370bc564ddd04dcd0d8d49dcc530 100644 --- a/burpui/misc/auth/local.py +++ b/burpui/misc/auth/local.py @@ -19,6 +19,7 @@ class LocalLoader(BUIloader): """ self.app = app self.users = None + handler.name = self.name conf = self.app.conf if self.section in conf.options: # Maybe the handler argument is None, maybe the 'priority' @@ -85,6 +86,10 @@ class UserHandler(BUIhandler): self.users[name] = LocalUser(self.local, name) return self.users[name] + @property + def loader(self): + return self.local + class LocalUser(BUIuser): """See :class:`burpui.misc.auth.interface.BUIuser`"""