mirror of
https://github.com/ziirish/burp-ui.git
synced 2026-05-15 14:16:08 -06:00
allow to run remove_client asynchronously through celery (#232)
This commit is contained in:
parent
3dabaf7690
commit
bb41c71fa4
6 changed files with 255 additions and 19 deletions
|
|
@ -593,6 +593,7 @@ class ClientSettings(Resource):
|
|||
responses={
|
||||
200: 'Success',
|
||||
403: 'Insufficient permissions',
|
||||
409: 'Conflict',
|
||||
500: 'Internal failure',
|
||||
}
|
||||
)
|
||||
|
|
@ -603,6 +604,10 @@ class ClientSettings(Resource):
|
|||
not current_user.acl.is_client_rw(client, server):
|
||||
self.abort(403, 'You don\'t have rights on this server')
|
||||
|
||||
if bui.client.is_backup_running(client, server):
|
||||
self.abort(409, 'There is currently a backup running for this client hence '
|
||||
'we cannot delete it for now. Please try again later')
|
||||
|
||||
args = self.parser_delete.parse_args()
|
||||
delcert = args.get('delcert', False)
|
||||
revoke = args.get('revoke', False)
|
||||
|
|
@ -625,7 +630,10 @@ class ClientSettings(Resource):
|
|||
force_scheduling_now()
|
||||
parser = bui.client.get_parser(agent=server)
|
||||
|
||||
bui.audit.logger.info(f'deleted client configuration {client} ({conf}), delete certificate: {delcert}, revoke certificate: {revoke}, keep a backup of the configuration: {keepconf}, delete data: {delete}', server=server)
|
||||
bui.audit.logger.info(
|
||||
f'deleted client configuration {client}, delete certificate: {delcert}, '
|
||||
f'revoke certificate: {revoke}, keep a backup of the configuration: '
|
||||
f'{keepconf}, delete data: {delete}, is template: {template}', server=server)
|
||||
return parser.remove_client(client, keepconf, delcert, revoke, template, delete), 200
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ from ..engines.server import BUIServer # noqa
|
|||
from ..ext.cache import cache
|
||||
from ..config import config
|
||||
from ..decorators import browser_cache
|
||||
from ..tasks import perform_restore, load_all_tree
|
||||
from ..tasks import perform_restore, load_all_tree, delete_client, force_scheduling_now
|
||||
|
||||
from time import time
|
||||
from zlib import adler32
|
||||
from flask import url_for, Response, current_app, after_this_request, \
|
||||
send_file, request
|
||||
send_file, request, g, session
|
||||
from flask_babel import gettext as _
|
||||
from flask_restplus import inputs
|
||||
from flask_login import current_user
|
||||
from datetime import timedelta
|
||||
from werkzeug.datastructures import Headers
|
||||
|
|
@ -44,9 +46,11 @@ else:
|
|||
bui = current_app # type: BUIServer
|
||||
ns = api.namespace('tasks', 'Asynchronous tasks methods')
|
||||
|
||||
# tuple composed with <task>, <callback url>
|
||||
task_types = {
|
||||
'restore': (perform_restore, '.task_get_file'),
|
||||
'browse': (load_all_tree, '.task_do_browse_all'),
|
||||
'delete': (delete_client, '.task_deleted_client'),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -82,7 +86,7 @@ class TaskStatus(Resource):
|
|||
def get(self, task_type, task_id, server=None):
|
||||
"""Returns the state of the given task"""
|
||||
if task_type not in task_types:
|
||||
return {'state': 'FAILURE'}
|
||||
self.abort(400)
|
||||
task_obj, callback = task_types[task_type]
|
||||
task = task_obj.AsyncResult(task_id)
|
||||
if task.state == 'FAILURE':
|
||||
|
|
@ -121,7 +125,7 @@ class TaskStatus(Resource):
|
|||
def delete(self, task_type, task_id, server=None):
|
||||
"""Cancel a given task"""
|
||||
if task_type not in task_types:
|
||||
return '', 400
|
||||
self.abort(400)
|
||||
task_obj, _ = task_types[task_type]
|
||||
task = task_obj.AsyncResult(task_id)
|
||||
user = task.result.get('user')
|
||||
|
|
@ -332,6 +336,14 @@ class TaskRestore(Resource):
|
|||
help='List of files/directories to restore',
|
||||
nullable=False
|
||||
)
|
||||
parser.add_argument(
|
||||
'timeout',
|
||||
type=int,
|
||||
required=False,
|
||||
help='Maximum task duration after you consider it stalled (in minutes)',
|
||||
default=60,
|
||||
nullable=True
|
||||
)
|
||||
|
||||
@ns.expect(parser, validate=True)
|
||||
@ns.doc(
|
||||
|
|
@ -363,6 +375,7 @@ class TaskRestore(Resource):
|
|||
strip = args['strip']
|
||||
fmt = args['format'] or 'zip'
|
||||
passwd = args['pass']
|
||||
timeout = args['timeout']
|
||||
args_log = args.copy()
|
||||
# don't leak secrets in logs
|
||||
del args_log['pass']
|
||||
|
|
@ -399,7 +412,7 @@ class TaskRestore(Resource):
|
|||
task.id,
|
||||
'perform_restore',
|
||||
current_user.name,
|
||||
timedelta(minutes=60)
|
||||
timedelta(minutes=timeout)
|
||||
)
|
||||
try:
|
||||
db.session.add(db_task)
|
||||
|
|
@ -409,6 +422,162 @@ class TaskRestore(Resource):
|
|||
return {'id': task.id, 'name': 'perform_restore'}, 202
|
||||
|
||||
|
||||
@ns.route('/config/<client>',
|
||||
'/config/<client>/<path:conf>',
|
||||
'/<server>/config/<client>',
|
||||
'/<server>/config/<client>/<path:conf>',
|
||||
endpoint='task_delete_client',
|
||||
methods=['DELETE'])
|
||||
@ns.doc(
|
||||
params={
|
||||
'server': 'Which server to collect data from when in multi-agent mode',
|
||||
'client': 'Client name',
|
||||
'conf': 'Path of the configuration file',
|
||||
},
|
||||
)
|
||||
class ClientSettings(Resource):
|
||||
parser_delete = ns.parser()
|
||||
parser_delete.add_argument('revoke', type=inputs.boolean, help='Whether to revoke the certificate or not', default=False, nullable=True)
|
||||
parser_delete.add_argument('delcert', type=inputs.boolean, help='Whether to delete the certificate or not', default=False, nullable=True)
|
||||
parser_delete.add_argument('keepconf', type=inputs.boolean, help='Whether to keep the conf or not', default=False, nullable=True)
|
||||
parser_delete.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True)
|
||||
parser_delete.add_argument('delete', type=inputs.boolean, help='Whether we should remove the data as well or not', default=False, nullable=True)
|
||||
|
||||
@api.disabled_on_demo()
|
||||
@api.acl_admin_or_moderator_required(message=_('Sorry, you don\'t have rights to access the setting panel'))
|
||||
@ns.expect(parser_delete)
|
||||
@ns.doc(
|
||||
responses={
|
||||
200: 'Success',
|
||||
403: 'Insufficient permissions',
|
||||
409: 'Conflict',
|
||||
500: 'Internal failure',
|
||||
}
|
||||
)
|
||||
def delete(self, server=None, client=None, conf=None):
|
||||
"""Deletes a given client"""
|
||||
if not current_user.is_anonymous and \
|
||||
current_user.acl.is_moderator() and \
|
||||
not current_user.acl.is_server_rw(server):
|
||||
self.abort(403, 'You don\'t have rights on this server')
|
||||
|
||||
if bui.client.is_backup_running(client, server):
|
||||
self.abort(409, 'There is currently a backup running for this client hence '
|
||||
'we cannot delete it for now. Please try again later')
|
||||
|
||||
args = self.parser_delete.parse_args()
|
||||
delcert = args.get('delcert', False)
|
||||
revoke = args.get('revoke', False)
|
||||
keepconf = args.get('keepconf', False)
|
||||
template = args.get('template', False)
|
||||
delete = args.get('delete', False)
|
||||
|
||||
task = delete_client.apply_async(
|
||||
args=[
|
||||
client,
|
||||
keepconf,
|
||||
delcert,
|
||||
revoke,
|
||||
template,
|
||||
delete,
|
||||
server,
|
||||
current_user.name
|
||||
]
|
||||
)
|
||||
|
||||
if db:
|
||||
db_task = Task(
|
||||
task.id,
|
||||
'delete_client',
|
||||
current_user.name,
|
||||
timedelta(minutes=60)
|
||||
)
|
||||
try:
|
||||
db.session.add(db_task)
|
||||
db.session.commit()
|
||||
except:
|
||||
db.session.rollback()
|
||||
return {'id': task.id, 'name': 'delete_client'}, 202
|
||||
|
||||
|
||||
@ns.route('/completed/config/<task_id>',
|
||||
'/completed/<server>/config/<task_id>',
|
||||
endpoint='task_deleted_client')
|
||||
@ns.doc(
|
||||
params={
|
||||
'task_id': 'The task ID to process',
|
||||
}
|
||||
)
|
||||
class TaskDeletedClient(Resource):
|
||||
"""The :class:`burpui.api.tasks.TaskDeletedClient` resource allows you to
|
||||
retrieve the result of the delete_client task.
|
||||
|
||||
This resource is part of the :mod:`burpui.api.tasks` module.
|
||||
"""
|
||||
|
||||
@ns.doc(
|
||||
responses={
|
||||
400: 'Incomplete task',
|
||||
403: 'Insufficient permissions',
|
||||
500: 'Task failed',
|
||||
},
|
||||
)
|
||||
def get(self, task_id, server=None):
|
||||
"""Returns the task result"""
|
||||
task = delete_client.AsyncResult(task_id)
|
||||
if task.state != 'SUCCESS':
|
||||
if task.state == 'FAILURE':
|
||||
self.abort(
|
||||
500,
|
||||
'Unsuccessful task: {}'.format(task.result.get('error'))
|
||||
)
|
||||
self.abort(400, 'Task not processed yet: {}'.format(task.state))
|
||||
|
||||
tres = task.result
|
||||
|
||||
user = tres.get('user')
|
||||
dst_server = tres.get('server')
|
||||
resp = tres.get('result')
|
||||
kwargs = tres.get('kwargs')
|
||||
|
||||
client = tres.get('client')
|
||||
delcert = kwargs.get('delcert')
|
||||
revoke = kwargs.get('revoke')
|
||||
keepconf = kwargs.get('keepconf')
|
||||
delete = kwargs.get('delete')
|
||||
template = kwargs.get('template')
|
||||
|
||||
if current_user.name != user or (dst_server and dst_server != server):
|
||||
self.abort(403, 'Unauthorized access')
|
||||
|
||||
task.revoke()
|
||||
|
||||
if not keepconf:
|
||||
# clear the cache when we remove a client
|
||||
cache.clear()
|
||||
# clear client-side cache through the _extra META variable
|
||||
try:
|
||||
_extra = session.get('_extra', g.now)
|
||||
_extra = int(_extra)
|
||||
except ValueError:
|
||||
_extra = 0
|
||||
session['_extra'] = '{}'.format(_extra + 1)
|
||||
if bui.config['WITH_CELERY']:
|
||||
force_scheduling_now()
|
||||
|
||||
bui.audit.logger.info(
|
||||
f'deleted client configuration {client}, delete certificate: {delcert}, '
|
||||
f'revoke certificate: {revoke}, keep a backup of the configuration: '
|
||||
f'{keepconf}, delete data: {delete}, is template: {template}', server=server)
|
||||
return resp
|
||||
|
||||
# if not keepconf:
|
||||
# parser = bui.client.get_parser(agent=server)
|
||||
#
|
||||
# bui.audit.logger.info(f'deleted client configuration {client} ({conf}), delete certificate: {delcert}, revoke certificate: {revoke}, keep a backup of the configuration: {keepconf}, delete data: {delete}', server=server)
|
||||
# return parser.remove_client(client, keepconf, delcert, revoke, template, delete), 200
|
||||
|
||||
|
||||
@ns.route('/running',
|
||||
'/<server>/running',
|
||||
'/running/<client>',
|
||||
|
|
|
|||
|
|
@ -462,6 +462,8 @@ class NClient(BUIbackend):
|
|||
if data['func'] == 'get_tree' and data['args'].get('root') == '*':
|
||||
# arbitrary raise timeout
|
||||
timeout = max(timeout, 300)
|
||||
if data['func'] == 'proxy_parser' and data['method'] == 'remove_client':
|
||||
notimeout = True
|
||||
try:
|
||||
# don't need a context manager here
|
||||
if data['func'] == 'get_file':
|
||||
|
|
|
|||
|
|
@ -318,13 +318,31 @@ def perform_restore(self, client, backup,
|
|||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def delete_client(self, client, keepconf, delcert, revoke, template, delete, server):
|
||||
def delete_client(self, client, keepconf, delcert, revoke, template, delete, server, user):
|
||||
parser = bui.client.get_parser(agent=server)
|
||||
self.update_state(state='STARTED', meta={'step': 'doing'})
|
||||
|
||||
ret = parser.remove_client(client, keepconf, delcert, revoke, template, delete)
|
||||
if any(x == NOTIF_ERROR for x, _ in ret):
|
||||
raise Exception
|
||||
res = parser.remove_client(client, keepconf, delcert, revoke, template, delete)
|
||||
if any(x == NOTIF_ERROR for x, _ in res):
|
||||
self.update_state(state='FAILURE', meta={'error': res})
|
||||
raise Exception(res)
|
||||
|
||||
ret = {
|
||||
'result': res,
|
||||
'client': client,
|
||||
'server': server,
|
||||
'user': user,
|
||||
'kwargs': {
|
||||
'keepconf': keepconf,
|
||||
'delcert': delcert,
|
||||
'revoke': revoke,
|
||||
'template': template,
|
||||
'delete': delete,
|
||||
'template': template,
|
||||
},
|
||||
}
|
||||
logger.debug(ret)
|
||||
return ret
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ $( document ).ready(function() {
|
|||
failCallback: function (responseHtml, url) {
|
||||
$preparingFileModal.modal('hide');
|
||||
if (responseHtml == 'encrypted') {
|
||||
msg = '{{ _("The backup seems encrypted, please provide the encryption key in the \\\'Download options\\\' form.") }}';
|
||||
msg = '{{ _("The backup seems encrypted, please provide the encryption key in the \'Download options\' form.")|escape }}';
|
||||
} else {
|
||||
msg = responseHtml;
|
||||
}
|
||||
|
|
@ -272,7 +272,7 @@ $( document ).ready(function() {
|
|||
return false;
|
||||
}
|
||||
if (resp == 'encrypted') {
|
||||
msg = '{{ _("The backup seems encrypted, please provide the encryption key in the \\\'Download options\\\' form.") }}';
|
||||
msg = '{{ _("The backup seems encrypted, please provide the encryption key in the \'Download options\' form.")|escape }}';
|
||||
} else {
|
||||
msg = resp;
|
||||
}
|
||||
|
|
@ -297,7 +297,7 @@ $( document ).ready(function() {
|
|||
failCallback: function (responseHtml, url) {
|
||||
$preparingFileModal.modal('hide');
|
||||
if (responseHtml == 'encrypted') {
|
||||
msg = 'The backup seems encrypted, please provide the encryption key in the \'Download options\' form.';
|
||||
msg = '{{ _("The backup seems encrypted, please provide the encryption key in the \'Download options\' form.")|escape }}';
|
||||
} else {
|
||||
msg = responseHtml;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,10 +628,23 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
|
|||
$scope.deleteClient = function() {
|
||||
/* UX tweak: disable the submit button + change text */
|
||||
submit = $('#btn-remove-client');
|
||||
parse_result = function(data) {
|
||||
redirect = data[0][0] == NOTIF_SUCCESS;
|
||||
notifAll(data, redirect);
|
||||
if (redirect) {
|
||||
$timeout(function() {
|
||||
document.location = '{{ url_for("view.settings", server=server) }}';
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
sav = submit.html();
|
||||
submit.html('<i class="fa fa-fw fa-spinner fa-pulse" aria-hidden="true"></i> {{ _("Deleting...") }}');
|
||||
submit.attr('disabled', true);
|
||||
{% if config.WITH_CELERY -%}
|
||||
api = '{{ url_for("api.task_delete_client", client=client, server=server) }}';
|
||||
{% else -%}
|
||||
api = '{{ url_for("api.client_settings", client=client, server=server) }}';
|
||||
{% endif -%}
|
||||
$.ajax({
|
||||
url: api,
|
||||
type: 'DELETE',
|
||||
|
|
@ -643,13 +656,39 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
|
|||
})
|
||||
.fail(buiFail)
|
||||
.done(function(data) {
|
||||
redirect = data[0][0] == NOTIF_SUCCESS;
|
||||
notifAll(data, redirect);
|
||||
if (redirect) {
|
||||
$timeout(function() {
|
||||
document.location = '{{ url_for("view.settings", server=server) }}';
|
||||
}, 1000);
|
||||
{% if config.WITH_CELERY -%}
|
||||
notif(NOTIF_SUCCESS, '{{ _("The client %(client)s is being deleted", client=client) }}');
|
||||
if ($('#deldata').is(':checked')) {
|
||||
notif(NOTIF_INFO, '{{ _("The data are being deleted in the background, you can leave this page if you like though the client may still shows up in the interface until the task completes") }}');
|
||||
}
|
||||
var _check_task_schedule = undefined;
|
||||
var check_task = function(task_id) {
|
||||
$.getJSON('{{ url_for("api.task_status", task_type="delete", task_id="", server=server) }}'+task_id)
|
||||
.done(function(d2) {
|
||||
if (d2.state != 'SUCCESS') {
|
||||
_check_task_schedule = setTimeout(function() {
|
||||
check_task(task_id);
|
||||
}, 2000);
|
||||
} else {
|
||||
$.getJSON(d2.location).done(parse_result);
|
||||
/* reset the submit button state */
|
||||
submit.html(sav);
|
||||
submit.attr('disabled', false);
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, stat, err) {
|
||||
if (xhr.status != 502) {
|
||||
buiFail(xhr, stat, err);
|
||||
} else if ('responseJSON' in xhr && 'message' in xhr.responseJSON) {
|
||||
notifAll(JSON.parse(xhr.responseJSON.message));
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
check_task(data.id);
|
||||
{% else -%}
|
||||
parse_result(data);
|
||||
{% endif -%}
|
||||
})
|
||||
.always(function() {
|
||||
/* reset the submit button state */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue